From ad39da6814f655fc6a4fe879bef2ca078a4c11cc Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 7 Nov 2018 09:45:30 -0800 Subject: [PATCH] feat: add media access apis for mojave --- BUILD.gn | 1 + .../api/atom_api_system_preferences.cc | 4 + .../browser/api/atom_api_system_preferences.h | 6 + .../api/atom_api_system_preferences_mac.mm | 42 +++++ .../browser/ui/cocoa/atom_access_controller.h | 35 +++++ .../ui/cocoa/atom_access_controller.mm | 143 ++++++++++++++++++ filenames.gni | 2 + 7 files changed, 233 insertions(+) create mode 100644 atom/browser/ui/cocoa/atom_access_controller.h create mode 100644 atom/browser/ui/cocoa/atom_access_controller.mm diff --git a/BUILD.gn b/BUILD.gn index 48ebec5363fb5..3206b6009397f 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -601,6 +601,7 @@ if (is_mac) { sources = filenames.framework_sources libs = [ + "AVFoundation.framework", "Carbon.framework", "QuartzCore.framework", "Quartz.framework", diff --git a/atom/browser/api/atom_api_system_preferences.cc b/atom/browser/api/atom_api_system_preferences.cc index 2acaf4a72ea87..4bc472acbaa2d 100644 --- a/atom/browser/api/atom_api_system_preferences.cc +++ b/atom/browser/api/atom_api_system_preferences.cc @@ -89,6 +89,10 @@ void SystemPreferences::BuildPrototype( &SystemPreferences::GetAppLevelAppearance) .SetMethod("setAppLevelAppearance", &SystemPreferences::SetAppLevelAppearance) + .SetMethod("hasCameraAccess", &SystemPreferences::HasCameraAccess) + .SetMethod("hasMicrophoneAccess", &SystemPreferences::HasMicrophoneAccess) + .SetMethod("hasFullMediaAccess", &SystemPreferences::HasFullMediaAccess) + .SetMethod("askForMediaAccess", &SystemPreferences::AskForMediaAccess) #endif .SetMethod("isInvertedColorScheme", &SystemPreferences::IsInvertedColorScheme) diff --git a/atom/browser/api/atom_api_system_preferences.h b/atom/browser/api/atom_api_system_preferences.h index 47e166046ff00..7e0eb3fa8d2f6 100644 --- a/atom/browser/api/atom_api_system_preferences.h +++ b/atom/browser/api/atom_api_system_preferences.h @@ -90,6 +90,12 @@ class SystemPreferences : public mate::EventEmitter void RemoveUserDefault(const std::string& name); bool IsSwipeTrackingFromScrollEventsEnabled(); + bool HasCameraAccess(); + bool HasMicrophoneAccess(); + bool HasFullMediaAccess(); + + void AskForMediaAccess(mate::Arguments* args); + // TODO(MarshallOfSound): Write tests for these methods once we // are running tests on a Mojave machine v8::Local GetEffectiveAppearance(v8::Isolate* isolate); diff --git a/atom/browser/api/atom_api_system_preferences_mac.mm b/atom/browser/api/atom_api_system_preferences_mac.mm index 699f4c91faeda..45ba17f0e9a1d 100644 --- a/atom/browser/api/atom_api_system_preferences_mac.mm +++ b/atom/browser/api/atom_api_system_preferences_mac.mm @@ -4,12 +4,14 @@ #include "atom/browser/api/atom_api_system_preferences.h" +#include #include #import #include "atom/browser/mac/atom_application.h" #include "atom/browser/mac/dict_util.h" +#include "atom/browser/ui/cocoa/atom_access_controller.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/value_converter.h" #include "base/strings/sys_string_conversions.h" @@ -360,6 +362,46 @@ static bool FromV8(v8::Isolate* isolate, } } +bool SystemPreferences::HasCameraAccess() { + return [[AtomAccessController sharedController] hasCameraAccess]; +} + +bool SystemPreferences::HasMicrophoneAccess() { + return [[AtomAccessController sharedController] hasMicrophoneAccess]; +} + +// whether the system has access to both microphone and camera +bool SystemPreferences::HasFullMediaAccess() { + return [[AtomAccessController sharedController] hasFullMediaAccess]; +} + +// ask for access to camera and/or microphone +void SystemPreferences::AskForMediaAccess(mate::Arguments* args) { + std::string media_type; + bool ask_again = false; + if (!args->GetNext(&ask_again)) + args->ThrowError("Ask again value required"); + + if (args->GetNext(&media_type)) { + std::cout << media_type << "\n"; + if (media_type == "microphone") + [[AtomAccessController sharedController] askForMicrophoneAccess]; + else if (media_type == "camera") + [[AtomAccessController sharedController] askForCameraAccess]; + else + args->ThrowError("Invalid media type"); + } else { + [[AtomAccessController sharedController] + askForMediaAccess:ask_again + completion:^(BOOL granted) { + if (granted) + printf("Access granted!!\n"); + else + printf("Access denied!\n"); + }]; + } +} + void SystemPreferences::RemoveUserDefault(const std::string& name) { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; [defaults removeObjectForKey:base::SysUTF8ToNSString(name)]; diff --git a/atom/browser/ui/cocoa/atom_access_controller.h b/atom/browser/ui/cocoa/atom_access_controller.h new file mode 100644 index 0000000000000..f1460258ea8dc --- /dev/null +++ b/atom/browser/ui/cocoa/atom_access_controller.h @@ -0,0 +1,35 @@ +// Copyright (c) 2018 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#import + +#ifndef ATOM_BROWSER_UI_COCOA_ATOM_ACCESS_CONTROLLER_H_ +#define ATOM_BROWSER_UI_COCOA_ATOM_ACCESS_CONTROLLER_H_ + +typedef NS_ENUM(NSInteger, AccessState) { + AccessStateUnknown, + AccessStateGranted, + AccessStateDenied +}; + +@interface AtomAccessController : NSObject { + AccessState microphoneAccessState_; + AccessState cameraAccessState_; +} + ++ (instancetype)sharedController; + +- (void)askForMediaAccess:(BOOL)askAgain + completion:(void (^)(BOOL))allAccessGranted; + +- (void)askForMicrophoneAccess; +- (void)askForCameraAccess; + +- (BOOL)hasMicrophoneAccess; +- (BOOL)hasCameraAccess; +- (BOOL)hasFullMediaAccess; + +@end + +#endif // ATOM_BROWSER_UI_COCOA_ATOM_ACCESS_CONTROLLER_H_ diff --git a/atom/browser/ui/cocoa/atom_access_controller.mm b/atom/browser/ui/cocoa/atom_access_controller.mm new file mode 100644 index 0000000000000..b448b595fed80 --- /dev/null +++ b/atom/browser/ui/cocoa/atom_access_controller.mm @@ -0,0 +1,143 @@ +#include "atom/browser/ui/cocoa/atom_access_controller.h" + +#import +#import + +@implementation AtomAccessController + ++ (instancetype)sharedController { + static dispatch_once_t once; + static AtomAccessController* sharedController; + dispatch_once(&once, ^{ + sharedController = [[self alloc] init]; + }); + return sharedController; +} + +- (instancetype)init { + if ((self = [super init])) { + if (@available(macOS 10.14, *)) { + switch ( + [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) { + case AVAuthorizationStatusAuthorized: + case AVAuthorizationStatusRestricted: + cameraAccessState_ = AccessStateDenied; + break; + case AVAuthorizationStatusDenied: + cameraAccessState_ = AccessStateDenied; + break; + case AVAuthorizationStatusNotDetermined: + cameraAccessState_ = AccessStateDenied; + } + switch ( + [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) { + case AVAuthorizationStatusAuthorized: + case AVAuthorizationStatusRestricted: + microphoneAccessState_ = AccessStateDenied; + break; + case AVAuthorizationStatusDenied: + microphoneAccessState_ = AccessStateDenied; + break; + case AVAuthorizationStatusNotDetermined: + microphoneAccessState_ = AccessStateDenied; + } + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver:self + selector:@selector(applicationLaunched:) + name:NSWorkspaceDidLaunchApplicationNotification + object:nil]; + } else { + cameraAccessState_ = AccessStateGranted; + microphoneAccessState_ = AccessStateDenied; + } + } + return self; +} + +- (void)askForMicrophoneAccess { + if (microphoneAccessState_ == AccessStateDenied) { + NSAlert* alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = @"This app needs access to the microphone."; + [alert addButtonWithTitle:@"Change Preferences"]; + [alert addButtonWithTitle:@"Cancel"]; + NSInteger modalResponse = [alert runModal]; + if (modalResponse == NSAlertFirstButtonReturn) { + [[NSWorkspace sharedWorkspace] + openURL:[NSURL + URLWithString:@"x-apple.systempreferences:com.apple." + @"preference.security?Privacy_Microphone"]]; + } + } +} + +- (void)askForCameraAccess { + if (cameraAccessState_ == AccessStateDenied) { + NSAlert* alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleWarning; + alert.messageText = @"This app needs access to the camera."; + [alert addButtonWithTitle:@"Change Preferences"]; + [alert addButtonWithTitle:@"Cancel"]; + NSInteger modalResponse = [alert runModal]; + if (modalResponse == NSAlertFirstButtonReturn) { + [[NSWorkspace sharedWorkspace] + openURL:[NSURL URLWithString:@"x-apple.systempreferences:com.apple." + @"preference.security?Privacy_Camera"]]; + } + } +} + +- (void)askForMediaAccess:(BOOL)askAgain + completion:(void (^)(BOOL))allAccessGranted { + if (@available(macOS 10.14, *)) { + [AVCaptureDevice + requestAccessForMediaType:AVMediaTypeAudio + completionHandler:^(BOOL granted) { + microphoneAccessState_ = + (granted) ? AccessStateGranted : AccessStateDenied; + [AVCaptureDevice + requestAccessForMediaType:AVMediaTypeVideo + completionHandler:^(BOOL granted) { + cameraAccessState_ = (granted) + ? AccessStateGranted + : AccessStateDenied; + if (askAgain) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self askForMicrophoneAccess]; + [self askForCameraAccess]; + }); + } + dispatch_async(dispatch_get_main_queue(), ^{ + allAccessGranted(self.hasFullMediaAccess); + }); + }]; + }]; + } else { + allAccessGranted(self.hasFullMediaAccess); + } +} + +- (BOOL)mediaConsentStatusKnown { + return ((microphoneAccessState_ != AccessStateUnknown) && + (cameraAccessState_ != AccessStateUnknown)); +} + +- (BOOL)hasCameraAccess { + if (@available(macOS 10.14, *)) { + return (cameraAccessState_ == AccessStateGranted); + } + return YES; +} + +- (BOOL)hasMicrophoneAccess { + if (@available(macOS 10.14, *)) { + return (microphoneAccessState_ == AccessStateGranted); + } + return YES; +} + +- (BOOL)hasFullMediaAccess { + return (self.hasCameraAccess && self.hasMicrophoneAccess); +} + +@end \ No newline at end of file diff --git a/filenames.gni b/filenames.gni index 7ce785819ed94..072e3f6843224 100644 --- a/filenames.gni +++ b/filenames.gni @@ -390,6 +390,8 @@ filenames = { "atom/browser/ui/certificate_trust.h", "atom/browser/ui/certificate_trust_mac.mm", "atom/browser/ui/certificate_trust_win.cc", + "atom/browser/ui/cocoa/atom_access_controller.h", + "atom/browser/ui/cocoa/atom_access_controller.mm", "atom/browser/ui/cocoa/atom_bundle_mover.h", "atom/browser/ui/cocoa/atom_bundle_mover.mm", "atom/browser/ui/cocoa/atom_menu_controller.h",