mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-22 12:13:03 +00:00
Request user permission to use input devices and Apple Events.
This is required to build against the macOS 10.14 SDK because 10.14 requires users to grant apps permission before they can use audio input devices or send Apple Events to other apps. I think builds built against the 10.13 SDK were supposed to continue working, but I haven't tested it. Note that without NSMicrophoneUsageDescription and NSAppleEventsUsageDescription, 10.14 builds will fail more or less silently when they try to use those features. (tccd does log a message about it, though.) See #163.
This commit is contained in:
parent
08fdef6084
commit
75e8d5ceac
17 changed files with 259 additions and 65 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,6 +6,7 @@
|
|||
tags
|
||||
cmake-build-debug/
|
||||
/Background-Music-*/
|
||||
BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
|
||||
|
||||
# Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore
|
||||
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C837DDA1F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; };
|
||||
1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; };
|
||||
1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; };
|
||||
1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; };
|
||||
|
@ -262,6 +264,7 @@
|
|||
1C8034C31BDAFD5700668E00 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
|
||||
1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputVolumeMenuItem.h; sourceTree = "<group>"; };
|
||||
1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputVolumeMenuItem.mm; sourceTree = "<group>"; };
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
1C8D8301204238DB00A838F2 /* Swinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Swinsian.h; path = "Music Players/Swinsian.h"; sourceTree = "<group>"; };
|
||||
1C8D8302204238DB00A838F2 /* BGMSwinsian.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSwinsian.m; path = "Music Players/BGMSwinsian.m"; sourceTree = "<group>"; };
|
||||
1C8D8303204238DB00A838F2 /* BGMSwinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSwinsian.h; path = "Music Players/BGMSwinsian.h"; sourceTree = "<group>"; };
|
||||
|
@ -358,6 +361,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */,
|
||||
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */,
|
||||
1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */,
|
||||
|
@ -368,6 +372,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */,
|
||||
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -692,6 +697,7 @@
|
|||
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1C8B0C69216205BF008C5679 /* AVFoundation.framework */,
|
||||
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
|
||||
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
|
||||
1C1963021BCAC160008A4DF7 /* CoreAudio.framework */,
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
|
@ -68,7 +67,6 @@
|
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableAddressSanitizer = "YES"
|
||||
language = ""
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
|
|
@ -40,6 +40,9 @@
|
|||
// PublicUtility Includes
|
||||
#import "CAPropertyAddress.h"
|
||||
|
||||
// System Includes
|
||||
#import <AVFoundation/AVCaptureDevice.h>
|
||||
|
||||
|
||||
#pragma clang assume_nonnull begin
|
||||
|
||||
|
@ -161,6 +164,9 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
|||
return;
|
||||
}
|
||||
|
||||
// Make BGMDevice the default device.
|
||||
[self setBGMDeviceAsDefault];
|
||||
|
||||
// Handle some of the unusual reasons BGMApp might have to exit, mostly crashes.
|
||||
BGMTermination::SetUpTerminationCleanUp(audioDevices);
|
||||
|
||||
|
@ -182,9 +188,6 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
|||
error);
|
||||
[self showXPCHelperErrorMessage:error];
|
||||
}];
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Returns NO if (and only if) BGMApp is about to terminate because of a fatal error.
|
||||
|
@ -197,18 +200,53 @@ static NSString* const kOptShowDockIcon = @"--show-dock-icon";
|
|||
return NO;
|
||||
}
|
||||
|
||||
error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set the Background Music device as your"
|
||||
"default audio device."
|
||||
informativeText:@"You might be able to set it yourself."];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Sets the "Background Music" virtual audio device (BGMDevice) as the user's default audio device.
|
||||
- (void) setBGMDeviceAsDefault {
|
||||
void (^setDefaultDevice)() = ^{
|
||||
NSError* error = [audioDevices setBGMDeviceAsOSDefault];
|
||||
|
||||
if (error) {
|
||||
[self showSetDeviceAsDefaultError:error
|
||||
message:@"Could not set the Background Music device as your"
|
||||
"default audio device."
|
||||
informativeText:@"You might be able to change it yourself."];
|
||||
}
|
||||
};
|
||||
|
||||
if (@available(macOS 10.14, *)) {
|
||||
// On macOS 10.14+ we need to get the user's permission to use input devices before we can
|
||||
// use BGMDevice for playthrough (see BGMPlayThrough), so we wait until they've given it
|
||||
// before making BGMDevice the default device. This way, if the user is playing audio when
|
||||
// they open Background Music, we won't interrupt it while we're waiting for them to click
|
||||
// OK.
|
||||
//
|
||||
// TODO: This isn't a perfect solution because, if the user takes too long to accept,
|
||||
// BGMPlayThrough will try to use BGMDevice again and log some errors.
|
||||
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio
|
||||
completionHandler:^(BOOL granted) {
|
||||
if (granted) {
|
||||
DebugMsg("BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission granted");
|
||||
setDefaultDevice();
|
||||
} else {
|
||||
NSLog(@"BGMAppDelegate::setBGMDeviceAsDefault: "
|
||||
"Permission denied");
|
||||
// TODO: If they don't accept, Background Music won't work
|
||||
// at all and the only way to fix it is in System
|
||||
// Preferences, so we should show an error dialog
|
||||
// with instructions.
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
// We can change the device immediately on older versions of macOS because they don't
|
||||
// require user permission for input devices.
|
||||
setDefaultDevice();
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setUpMainMenu:(BGMUserDefaults*)userDefaults {
|
||||
autoPauseMenuItem =
|
||||
[[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped
|
||||
|
|
|
@ -28,12 +28,16 @@
|
|||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>Background Music needs to control your music player app if you want it to automatically pause your music.</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016-2018 Background Music contributors</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The "Background Music" virtual audio device sends system audio to Background Music (the app) through a virtual input device.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSServices</key>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMDecibel.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Copyright © 2016 Tanner Hoke
|
||||
//
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"A9790CD5-4886-47C7-9FFC-DD70743CF2BF"]
|
||||
name:@"Decibel"
|
||||
bundleID:@"org.sbooth.Decibel"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -54,6 +54,11 @@
|
|||
return (DecibelApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.decibel.running;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMHermes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -44,7 +44,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"0CDC67B0-56D3-4D94-BC06-6E380D8F5E34"]
|
||||
name:@"Hermes"
|
||||
bundleID:@"com.alexcrichton.Hermes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -54,6 +54,11 @@
|
|||
return (HermesApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.hermes is nil (i.e. Hermes isn't running).
|
||||
return self.hermes.running;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayer.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
// The base classes and protocol for objects that represent a music player app.
|
||||
//
|
||||
|
@ -84,6 +84,9 @@
|
|||
// BGMMusicPlayers could pass a pointer to itself to createInstances.
|
||||
@property NSNumber* __nullable pid;
|
||||
|
||||
// True if this is currently the selected music player.
|
||||
@property (readonly) BOOL selected;
|
||||
|
||||
// The state of the music player.
|
||||
//
|
||||
// True if the music player app is open.
|
||||
|
@ -97,10 +100,14 @@
|
|||
// BGMApp paused it.
|
||||
@property (readonly, getter=isPaused) BOOL paused;
|
||||
|
||||
// Called when the user selects this music player.
|
||||
- (void) onSelect;
|
||||
// Called when this is the selected music player and the user selects a different one.
|
||||
- (void) onDeselect;
|
||||
|
||||
// Pause the music player. Does nothing if the music player is already paused or isn't running.
|
||||
// Returns YES if the music player is paused now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) pause;
|
||||
|
||||
// Unpause the music player. Does nothing if the music player is already playing or isn't running.
|
||||
// Returns YES if the music player is playing now but wasn't before, returns NO otherwise.
|
||||
- (BOOL) unpause;
|
||||
|
@ -130,6 +137,9 @@
|
|||
@property (readonly) NSString* name;
|
||||
@property (readonly) NSString* __nullable bundleID;
|
||||
@property NSNumber* __nullable pid;
|
||||
@property (readonly) BOOL selected;
|
||||
- (void) onSelect;
|
||||
- (void) onDeselect;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayer.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -35,6 +35,7 @@
|
|||
@synthesize name = _name;
|
||||
@synthesize bundleID = _bundleID;
|
||||
@synthesize pid = _pid;
|
||||
@synthesize selected = _selected;
|
||||
|
||||
- (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID
|
||||
name:(NSString*)name
|
||||
|
@ -56,6 +57,7 @@
|
|||
_name = name;
|
||||
_bundleID = bundleID;
|
||||
_pid = pid;
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -82,6 +84,14 @@
|
|||
return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]);
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
_selected = YES;
|
||||
}
|
||||
|
||||
- (void) onDeselect {
|
||||
_selected = NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang assume_nonnull end
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMMusicPlayers.mm
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self include
|
||||
|
@ -206,6 +206,16 @@
|
|||
@"BGMMusicPlayers::setSelectedMusicPlayerImpl: Only the music players in the musicPlayers array can be selected. "
|
||||
"newSelectedMusicPlayer=%@",
|
||||
newSelectedMusicPlayer.name);
|
||||
|
||||
if (_selectedMusicPlayer == newSelectedMusicPlayer) {
|
||||
DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: %s is already the selected music "
|
||||
"player.",
|
||||
_selectedMusicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the current music player (object) a different player has been selected.
|
||||
[_selectedMusicPlayer onDeselect];
|
||||
|
||||
_selectedMusicPlayer = newSelectedMusicPlayer;
|
||||
|
||||
|
@ -217,6 +227,9 @@
|
|||
|
||||
// Save the new setting in user defaults.
|
||||
userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString;
|
||||
|
||||
// Tell the music player (object) it's been selected.
|
||||
[_selectedMusicPlayer onSelect];
|
||||
}
|
||||
|
||||
- (void) updateBGMDeviceMusicPlayerProperties {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMScriptingBridge.h
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016, 2018 Kyle Neideck
|
||||
//
|
||||
// A wrapper around Scripting Bridge's SBApplication that tries to avoid ever launching the application.
|
||||
//
|
||||
|
@ -29,6 +29,9 @@
|
|||
// unless the music player app is running. That way messages sent while the app is closed are ignored.
|
||||
//
|
||||
|
||||
// Local Includes
|
||||
#import "BGMMusicPlayer.h"
|
||||
|
||||
// System Includes
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <ScriptingBridge/ScriptingBridge.h>
|
||||
|
@ -38,12 +41,19 @@
|
|||
|
||||
@interface BGMScriptingBridge : NSObject <SBApplicationDelegate>
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)bundleID;
|
||||
// Only keeps a weak ref to musicPlayer.
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer;
|
||||
|
||||
// If the music player application is running, this property is the Scripting Bridge object representing
|
||||
// it. If not, it's set to nil. Used to send Apple events to the music player app.
|
||||
@property (readonly) __kindof SBApplication* __nullable application;
|
||||
|
||||
// macOS 10.14 requires the user's permission to send Apple Events. If the music player that owns
|
||||
// this object (i.e. the one passed to initWithMusicPlayer) is currently the selected music player
|
||||
// and the user hasn't already given us permission to send it Apple Events, this method asks the
|
||||
// user for permission.
|
||||
- (void) ensurePermission;
|
||||
|
||||
// SBApplicationDelegate
|
||||
|
||||
// On 10.11, SBApplicationDelegate.h declares eventDidFail with a non-null return type, but the docs
|
||||
|
|
|
@ -17,12 +17,15 @@
|
|||
// BGMScriptingBridge.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
#import "BGMScriptingBridge.h"
|
||||
|
||||
// Local Includes
|
||||
#import "BGM_Utils.h"
|
||||
|
||||
// PublicUtility Includes
|
||||
#import "CADebugMacros.h"
|
||||
|
||||
|
@ -30,16 +33,16 @@
|
|||
#pragma clang assume_nonnull begin
|
||||
|
||||
@implementation BGMScriptingBridge {
|
||||
NSString* bundleID;
|
||||
id<BGMMusicPlayer> __weak _musicPlayer;
|
||||
// Tokens for the notification observers. We need these to remove the observers in dealloc.
|
||||
id didLaunchToken, didTerminateToken;
|
||||
id _didLaunchToken, _didTerminateToken;
|
||||
}
|
||||
|
||||
@synthesize application = _application;
|
||||
|
||||
- (instancetype) initWithBundleID:(NSString*)inBundleID {
|
||||
- (instancetype) initWithMusicPlayer:(id<BGMMusicPlayer>)musicPlayer {
|
||||
if ((self = [super init])) {
|
||||
bundleID = inBundleID;
|
||||
_musicPlayer = musicPlayer;
|
||||
|
||||
[self initApplication];
|
||||
}
|
||||
|
@ -48,9 +51,17 @@
|
|||
}
|
||||
|
||||
- (void) initApplication {
|
||||
NSString* bundleID = _musicPlayer.bundleID;
|
||||
BGMAssert(bundleID, "Music players need a bundle ID to use ScriptingBridge");
|
||||
|
||||
BGMScriptingBridge* __weak weakSelf = self;
|
||||
|
||||
void (^createSBApplication)(void) = ^{
|
||||
_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
_application.delegate = self;
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
strongSelf->_application = [SBApplication applicationWithBundleIdentifier:bundleID];
|
||||
// TODO: I think the SBApplication will still keep a strong ref to this object, so we might
|
||||
// have to make a separate delegate object.
|
||||
strongSelf->_application.delegate = strongSelf;
|
||||
};
|
||||
|
||||
BOOL (^isAboutThisMusicPlayer)(NSNotification*) = ^(NSNotification* note) {
|
||||
|
@ -66,26 +77,30 @@
|
|||
// "For applications that declare themselves to have a dynamic scripting interface, this method will
|
||||
// launch the application if it is not already running."
|
||||
NSNotificationCenter* center = [[NSWorkspace sharedWorkspace] notificationCenter];
|
||||
didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
}
|
||||
}];
|
||||
didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note) {
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
_application = nil;
|
||||
}
|
||||
}];
|
||||
_didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note)
|
||||
{
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s launched",
|
||||
bundleID.UTF8String);
|
||||
createSBApplication();
|
||||
[weakSelf ensurePermission];
|
||||
}
|
||||
}];
|
||||
_didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification* note)
|
||||
{
|
||||
if (isAboutThisMusicPlayer(note)) {
|
||||
DebugMsg("BGMScriptingBridge::initApplication: %s terminated",
|
||||
bundleID.UTF8String);
|
||||
BGMScriptingBridge* __strong strongSelf = weakSelf;
|
||||
strongSelf->_application = nil;
|
||||
}
|
||||
}];
|
||||
|
||||
// Create the SBApplication if the music player is already running.
|
||||
if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) {
|
||||
|
@ -97,15 +112,69 @@
|
|||
// Remove the application launch/termination observers.
|
||||
NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter;
|
||||
|
||||
if (didLaunchToken) {
|
||||
[center removeObserver:didLaunchToken];
|
||||
if (_didLaunchToken) {
|
||||
[center removeObserver:_didLaunchToken];
|
||||
}
|
||||
|
||||
if (didTerminateToken) {
|
||||
[center removeObserver:didTerminateToken];
|
||||
if (_didTerminateToken) {
|
||||
[center removeObserver:_didTerminateToken];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) ensurePermission {
|
||||
// Skip this check if running on a version of macOS before 10.14. In that case, we don't require
|
||||
// user permission to send Apple Events. Also skip it if compiling on an earlier version.
|
||||
#if MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 // MAC_OS_X_VERSION_10_14
|
||||
if (@available(macOS 10.14, *)) {
|
||||
id<BGMMusicPlayer> musicPlayer = _musicPlayer;
|
||||
|
||||
if (!musicPlayer.selected) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not selected. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!musicPlayer.running) {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: %s not running. Nothing to do.",
|
||||
musicPlayer.name.UTF8String);
|
||||
return;
|
||||
}
|
||||
|
||||
// AEDeterminePermissionToAutomateTarget will block if it has to show a dialog to the user
|
||||
// to ask for permission, so dispatch this to make sure it doesn't run on the main thread.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
||||
NSAppleEventDescriptor* musicPlayerEventDescriptor =
|
||||
[NSAppleEventDescriptor
|
||||
descriptorWithBundleIdentifier:(NSString*)musicPlayer.bundleID];
|
||||
|
||||
OSStatus status =
|
||||
AEDeterminePermissionToAutomateTarget(musicPlayerEventDescriptor.aeDesc,
|
||||
typeWildCard,
|
||||
typeWildCard,
|
||||
true);
|
||||
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: "
|
||||
"Apple Events permission status for %s: %d",
|
||||
musicPlayer.name.UTF8String,
|
||||
status);
|
||||
|
||||
if (status != noErr) {
|
||||
// TODO: If they deny permission, we should grey-out the auto-pause menu item and
|
||||
// add something to the UI that indicates the problem. Maybe a warning icon
|
||||
// that shows an explanation when you hover your mouse over it. (We can't just
|
||||
// ask them again later because the API doesn't support it. They can only fix
|
||||
// it in System Preferences.)
|
||||
NSLog(@"BGMScriptingBridge::ensurePermission: Permission denied for %@. status=%d",
|
||||
musicPlayer.name,
|
||||
status);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
DebugMsg("BGMScriptingBridge::ensurePermission: Not macOS 10.14+. Nothing to do.");
|
||||
}
|
||||
#endif /* MAC_OS_X_VERSION_MAX_ALLOWED >= 101400 */
|
||||
}
|
||||
|
||||
#pragma mark SBApplicationDelegate
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
@ -118,7 +187,7 @@
|
|||
NSString* vars = [NSString stringWithFormat:@"event='%4.4s' error=%@ application=%@",
|
||||
(char*)&(event->descriptorType), error, self.application];
|
||||
DebugMsg("BGMScriptingBridge::eventDidFail: Apple event sent to %s failed. %s",
|
||||
bundleID.UTF8String,
|
||||
_musicPlayer.bundleID.UTF8String,
|
||||
vars.UTF8String);
|
||||
#else
|
||||
#pragma unused (event, error)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMSpotify.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
// Spotify's AppleScript API looks to have been designed to match iTunes', so this file is basically
|
||||
// just s/iTunes/Spotify/ on BGMiTunes.m
|
||||
|
@ -47,7 +47,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"EC2A907F-8515-4687-9570-1BF63176E6D8"]
|
||||
name:@"Spotify"
|
||||
bundleID:@"com.spotify.client"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -57,6 +57,11 @@
|
|||
return (SpotifyApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.spotify is nil (i.e. Spotify isn't running).
|
||||
return self.spotify.running;
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:musicPlayerID
|
||||
name:@"Swinsian"
|
||||
bundleID:@"com.swinsian.Swinsian"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -57,6 +57,11 @@
|
|||
return (SwinsianApplication* __nullable)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
// Note that this will return NO if is self.swinsian is nil (i.e. Swinsian isn't running).
|
||||
return self.swinsian.running;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMVLC.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
// Portions copyright (C) 2012 Peter Ljunglöf. All rights reserved.
|
||||
//
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"]
|
||||
name:@"VLC"
|
||||
bundleID:@"org.videolan.vlc"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -54,6 +54,11 @@
|
|||
return (VLCApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vlc.running;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMVOX.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -43,7 +43,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"26498C5D-C18B-4689-8B41-9DA91A78FFAD"]
|
||||
name:@"VOX"
|
||||
bundleID:@"com.coppertino.Vox"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString*)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -53,6 +53,11 @@
|
|||
return (VoxApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.vox.running;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
// BGMiTunes.m
|
||||
// BGMApp
|
||||
//
|
||||
// Copyright © 2016 Kyle Neideck
|
||||
// Copyright © 2016-2018 Kyle Neideck
|
||||
//
|
||||
|
||||
// Self Include
|
||||
|
@ -49,7 +49,7 @@
|
|||
if ((self = [super initWithMusicPlayerID:[BGMiTunes sharedMusicPlayerID]
|
||||
name:@"iTunes"
|
||||
bundleID:@"com.apple.iTunes"])) {
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithBundleID:(NSString* __nonnull)self.bundleID];
|
||||
scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self];
|
||||
}
|
||||
|
||||
return self;
|
||||
|
@ -59,6 +59,11 @@
|
|||
return (iTunesApplication*)scriptingBridge.application;
|
||||
}
|
||||
|
||||
- (void) onSelect {
|
||||
[super onSelect];
|
||||
[scriptingBridge ensurePermission];
|
||||
}
|
||||
|
||||
- (BOOL) isRunning {
|
||||
return self.iTunes.running;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue