Privacy Consent in Mojave (part 1: media and documents)

MacOS Mojave brings new user control over applications’ access to user data, camera, microphone, and AppleScript automation. This two-part series describes our experience adopting the new privacy requirements in the Panopto Mac Recorder. We needed to smooth out the process for camera, microphone, and AppleScript, but our approach will work for any of the dozen or so privacy-restricted information categories.

 

Because the Panopto Mac Recorder is a video and audio capture application, we need to comply with Camera and Microphone privacy consent. Any call to AVFoundation that would grant access to camera or microphone data triggers an alert from the system, and an opportunity for the user to grant or deny access. 

 

However, the view controller that needs camera and microphone access has multiple previews, and a live audio level meter. The calls from AVFoundation to request access are asynchronous. That means that bringing up that one view controller triggers six different alerts in rapid succession, each asking for camera or microphone access. That’s not a user experience we want to present.

 

I talked with Tim Ekl about the problem. He said that Omni Group was using a single gatekeeper object to manage all of their privacy consent requests. That’s the approach we decided to take. A singleton PrivacyConsentController is now responsible for handling all of the privacy consent requests, and for recovering from rejection of consent.

 

The source code for PrivacyConsentController is available at https://github.com/Panopto/test-mac-privacy-consent under an Apache license.

 

The method -requestAccessForMediaType: on AVCaptureDevice requests access for audio and video devices. It takes a completion handler (as.a block), which is fired asynchronously after a one-time UI challenge. If the user has previously granted permission for access, the completion handler fires immediately. If it’s the first time requesting access, the completion handler fires after the user makes their choice. 

 

For simplicity’s sake, we require that the user grant access to both the camera and the microphone before we proceed to the recording preview screen. We ask for audio access first, and then, in the completion handler, ask for camera access. Finally, in the completion handler for the camera request, we fire a developer-supplied block on the main thread.

 

We need to support macOS versions back through 10.11. So we’ll wrap the logic in an @available clause, and always invoke the completion handler with a consent status of YES for macOS prior to 10.14. We track the consent status in a property, with a custom PrivacyConsentState enum having values for granted, denied, and unknown. We use the custom enum because the AVAuthorizationStatus enum (returned by –authorizationStatusForMediaType:) is not defined prior to 10.14, and we want to know the status on earlier OS versions.

 

There’s another complication, though. The user alert for each kind of privacy access (camera, microphone, calendar, etc) is only presented once for each application. If they clicked “grant”, that’s great, and we’re off and running. If they clicked “deny”, though, we’re stuck. We can’t present another request via the operating system, and we can’t bring up our recording preview.

 

Enter the nag screen. The nag screen points the user to the correct Privacy & Security pane. We will show the nag screen (optionally, depending on a parameter to our gatekeeper method) from the completion handler if permission is not granted.

 

Putting it all together, here’s what the IBAction looks like for macOS 10.14, with the guard code in place, restricting access to the AVFoundation-heavy view controller until we get the consent we need. This code works all the way back to macOS 10.11.

 

– (IBAction)newRecording:(id)sender

{

    [[PrivacyConsentController sharedController] requestMediaConsentNagIfDenied:YES completion:^(BOOL granted) {

        if (granted) {

            [self openCreateRecordingView];

        }

    }];

}

 

– (void)openCreateRecordingView

 

{

}

 

Here’s the entry point for media consent:

 

Screen Shot 2018 09 03 at 5 18 43 PM

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

– (void)requestMediaConsentNagIfDenied:(BOOL)nagIfDenied completion:(void (^)(BOOL))allMediaAccessGranted

{

    if (@available(macOS 10.14, *)) {

        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {

            if (granted) {

                self.microphoneConsentState = PrivacyConsentStateGranted;

            }

            else {

                self.microphoneConsentState = PrivacyConsentStateDenied;

            }

            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {

                if (granted) {

                    self.cameraConsentState = PrivacyConsentStateGranted;

                }

                else {

                    self.cameraConsentState = PrivacyConsentStateDenied;

                }

                if (nagIfDenied) {

                    dispatch_async(dispatch_get_main_queue(), ^{

                        [self nagForMicrophoneConsentIfNeeded];

                        [self nagForCameraConsentIfNeeded];

                    });

                }

                dispatch_async(dispatch_get_main_queue(), ^{

                    allMediaAccessGranted(self.hasFullMediaConsent);

                });

            }];

        }];

    }

    else {

        allMediaAccessGranted(self.hasFullMediaConsent);

    }

}

 

The call to -requestAccessForMediaType: is documented as taking some time to fire its completion handler. That is in fact the case when you’re asking for consent for the first time. But on the second and subsequent requests, the completion handler is in practice invoked immediately, with granted set to the user’s previous answer.

 

Here’s a sample nag screen, to recover from a denial of consent:

 

Screen Shot 2018 09 03 at 5 19 01 PM

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

– (void)nagForMicrophoneConsentIfNeeded

{

    if (self.microphoneConsentState == PrivacyConsentStateDenied) {

        NSAlert *alert = [[NSAlert alloc] init];

        alert.alertStyle = NSAlertStyleWarning;

        alert.messageText = @”Panopto needs access to the microphone”;

        alert.informativeText = @”Panopto can’t make recordings unless you grant permission for access to your microphone.”;

        [alert addButtonWithTitle:@”Change Security & Privacy Preferences”];

        [alert addButtonWithTitle:@”Cancel”];

        

        NSInteger modalResponse = [alert runModal];

        if (modalResponse == NSAlertFirstButtonReturn) {

            [self launchPrivacyAndSecurityPreferencesMicrophoneSubPane];

        }

    }

}

 

How do we respond to the alert? By linking to a URL that is not officially documented, using the x-apple.systempreferences: scheme. I worked out the URLs by starting with the links at https://macosxautomation.com/system-prefs-links.html, and then applied some guesswork. You can see many of the URL targets I found in the source code at https://github.com/Panopto/test-mac-privacy-consent.

 

– (void)launchPrivacyAndSecurityPreferencesMicrophoneSubPane

{

    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@”x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone”]];

}

Take note: when you’re working with camera, microphone, calendar, reminders, and other media-based access, your program’s privacy consents will NEVER change from PrivacyConsentStateDenied to PrivacyConsentStateGranted within a single run of your program. The user must quit and restart your program for the control panel’s consent to take effect. For standard media/calendar/reminders consent, your users will see a reminder to quit and restart your app. We will see in the next post that this is NOT the behavior for AppleScript consent.

Screen Shot 2018 09 04 at 10 56 24 AM

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

For testing, use the command line invocations “tcc reset All”, “tcc reset Camera”, “tcc reset Microphone”, or “tcc reset AppleEvents”.

Next up, in a separate post: how do we deal with AppleScript consent requests? It’s a bit more complicated.

2 thoughts on “Privacy Consent in Mojave (part 1: media and documents)

Comments are closed.