diff --git a/BUILD.gn b/BUILD.gn index 18ff9c8767250..619bf42dcbeae 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -558,6 +558,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 d008405fde98a..6a5f7d6ba6a70 100644 --- a/atom/browser/api/atom_api_system_preferences.cc +++ b/atom/browser/api/atom_api_system_preferences.cc @@ -83,6 +83,9 @@ void SystemPreferences::BuildPrototype( &SystemPreferences::GetAppLevelAppearance) .SetMethod("setAppLevelAppearance", &SystemPreferences::SetAppLevelAppearance) + .SetMethod("getMediaAccessStatus", + &SystemPreferences::GetMediaAccessStatus) + .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 04fd31f7159d6..4fa3c61ef5ff6 100644 --- a/atom/browser/api/atom_api_system_preferences.h +++ b/atom/browser/api/atom_api_system_preferences.h @@ -9,6 +9,7 @@ #include #include "atom/browser/api/event_emitter.h" +#include "atom/common/promise_util.h" #include "base/callback.h" #include "base/values.h" #include "native_mate/handle.h" @@ -90,6 +91,13 @@ class SystemPreferences : public mate::EventEmitter void RemoveUserDefault(const std::string& name); bool IsSwipeTrackingFromScrollEventsEnabled(); + // TODO(codebytere): Write tests for these methods once we + // are running tests on a Mojave machine + std::string GetMediaAccessStatus(const std::string& media_type, + mate::Arguments* args); + v8::Local AskForMediaAccess(v8::Isolate* isolate, + const std::string& media_type); + // 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..55c8f6efb5d44 100644 --- a/atom/browser/api/atom_api_system_preferences_mac.mm +++ b/atom/browser/api/atom_api_system_preferences_mac.mm @@ -6,6 +6,7 @@ #include +#import #import #include "atom/browser/mac/atom_application.h" @@ -78,6 +79,31 @@ static bool FromV8(v8::Isolate* isolate, // The map to convert |id| to |int|. std::map g_id_map; +AVMediaType ParseMediaType(const std::string& media_type) { + if (media_type == "camera") { + return AVMediaTypeVideo; + } else if (media_type == "microphone") { + return AVMediaTypeAudio; + } else { + return nil; + } +} + +std::string ConvertAuthorizationStatus(AVAuthorizationStatusMac status) { + switch (status) { + case AVAuthorizationStatusNotDeterminedMac: + return "not-determined"; + case AVAuthorizationStatusRestrictedMac: + return "restricted"; + case AVAuthorizationStatusDeniedMac: + return "denied"; + case AVAuthorizationStatusAuthorizedMac: + return "granted"; + default: + return "unknown"; + } +} + } // namespace void SystemPreferences::PostNotification( @@ -360,6 +386,47 @@ static bool FromV8(v8::Isolate* isolate, } } +std::string SystemPreferences::GetMediaAccessStatus( + const std::string& media_type, + mate::Arguments* args) { + if (auto type = ParseMediaType(media_type)) { + if (@available(macOS 10.14, *)) { + return ConvertAuthorizationStatus( + [AVCaptureDevice authorizationStatusForMediaType:type]); + } else { + // access always allowed pre-10.14 Mojave + return ConvertAuthorizationStatus(AVAuthorizationStatusAuthorizedMac); + } + } else { + args->ThrowError("Invalid media type"); + return std::string(); + } +} + +v8::Local SystemPreferences::AskForMediaAccess( + v8::Isolate* isolate, + const std::string& media_type) { + scoped_refptr promise = new util::Promise(isolate); + + if (auto type = ParseMediaType(media_type)) { + if (@available(macOS 10.14, *)) { + [AVCaptureDevice requestAccessForMediaType:type + completionHandler:^(BOOL granted) { + dispatch_async(dispatch_get_main_queue(), ^{ + promise->Resolve(!!granted); + }); + }]; + } else { + // access always allowed pre-10.14 Mojave + promise->Resolve(true); + } + } else { + promise->RejectWithErrorMessage("Invalid media type"); + } + + return promise->GetHandle(); +} + void SystemPreferences::RemoveUserDefault(const std::string& name) { NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; [defaults removeObjectForKey:base::SysUTF8ToNSString(name)]; diff --git a/atom/browser/mac/atom_application.h b/atom/browser/mac/atom_application.h index 7eae3813faf92..2f07676e3059d 100644 --- a/atom/browser/mac/atom_application.h +++ b/atom/browser/mac/atom_application.h @@ -6,7 +6,9 @@ #include "base/mac/scoped_nsobject.h" #include "base/mac/scoped_sending_event.h" -// Forward Declare Appareance APIs +#import + +// Forward Declare Appearance APIs @interface NSApplication (HighSierraSDK) @property(copy, readonly) NSAppearance* effectiveAppearance API_AVAILABLE(macosx(10.14)); @@ -14,6 +16,27 @@ - (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14)); @end +// forward declare Access APIs +typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM; + +AVF_EXPORT AVMediaType const AVMediaTypeVideo; +AVF_EXPORT AVMediaType const AVMediaTypeAudio; + +typedef NS_ENUM(NSInteger, AVAuthorizationStatusMac) { + AVAuthorizationStatusNotDeterminedMac = 0, + AVAuthorizationStatusRestrictedMac = 1, + AVAuthorizationStatusDeniedMac = 2, + AVAuthorizationStatusAuthorizedMac = 3, +}; + +@interface AVCaptureDevice (MojaveSDK) ++ (void)requestAccessForMediaType:(AVMediaType)mediaType + completionHandler:(void (^)(BOOL granted))handler + API_AVAILABLE(macosx(10.14)); ++ (AVAuthorizationStatusMac)authorizationStatusForMediaType: + (AVMediaType)mediaType API_AVAILABLE(macosx(10.14)); +@end + extern "C" { #if !defined(MAC_OS_X_VERSION_10_14) || \ MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14 diff --git a/docs/api/system-preferences.md b/docs/api/system-preferences.md index c9b0f9c13664b..27fa112d566fe 100644 --- a/docs/api/system-preferences.md +++ b/docs/api/system-preferences.md @@ -302,7 +302,6 @@ using `electron-packager` or `electron-forge` just set the `enableDarwinDarkMode packager option to `true`. See the [Electron Packager API](https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#darwindarkmodesupport) for more details. - ### `systemPreferences.getAppLevelAppearance()` _macOS_ Returns `String` | `null` - Can be `dark`, `light` or `unknown`. @@ -317,3 +316,21 @@ You can use the `setAppLevelAppearance` API to set this value. Sets the appearance setting for your application, this should override the system default and override the value of `getEffectiveAppearance`. + +### `systemPreferences.getMediaAccessStatus(mediaType)` _macOS_ + +* `mediaType` String - `microphone` or `camera`. + +Returns `String` - Can be `not-determined`, `granted`, `denied`, `restricted` or `unknown`. + +This user consent was not required until macOS 10.14 Mojave, so this method will always return `granted` if your system is running 10.13 High Sierra or lower. + +### `systemPreferences.askForMediaAccess(mediaType)` _macOS_ + +* `mediaType` String - the type of media being requested; can be `microphone`, `camera`. + +Returns `Promise` - A promise that resolves with `true` if consent was granted and `false` if it was denied. If an invalid `mediaType` is passed, the promise will be rejected. If an access request was denied and later is changed through the System Preferences pane, a restart of the app will be required for the new permissions to take effect. If access has already been requested and denied, it _must_ be changed through the preference pane; an alert will not pop up and the promise will resolve with the existing access status. + +**Important:** In order to properly leverage this API, you [must set](https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/requesting_authorization_for_media_capture_on_macos?language=objc) the `NSMicrophoneUsageDescription` and `NSCameraUsageDescription` strings in your app's `Info.plist` file. The values for these keys will be used to populate the permission dialogs so that the user will be properly informed as to the purpose of the permission request. See [Electron Application Distribution](https://electronjs.org/docs/tutorial/application-distribution#macos) for more information about how to set these in the context of Electron. + +This user consent was not required until macOS 10.14 Mojave, so this method will always return `true` if your system is running 10.13 High Sierra or lower. \ No newline at end of file