Signal-iOS/SignalUI/ViewControllers/UIViewController+Permissions.swift
Jordan Rose dd260bc990 Ensure that the video mute button in calls has the right initial state
Previously, if you answered a call on the lockscreen but hadn't given
Signal permission to use the camera, opening the app would show video
as unmuted but with no actual video being sent. By explicitly enabling
or disabling video when the call starts (and allowing the camera to be
considered "enabled" when the app is in the background, even though no
video will be sent until it returns to the foreground), the button is
always in the right state.
2023-07-25 10:59:07 -07:00

188 lines
7.0 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import Photos
import SignalCoreKit
import SignalMessaging
import SignalServiceKit
extension UIViewController {
public func ows_askForCameraPermissions(callback: @escaping (Bool) -> Void) {
Logger.verbose("\(String(describing: Self.self)) ows_askForCameraPermissions")
// Ensure callback is invoked on main thread.
let threadSafeCallback: (Bool) -> Void = { granted in
DispatchMainThreadSafe {
callback(granted)
}
}
guard UIImagePickerController.isSourceTypeAvailable(.camera) || Platform.isSimulator else {
Logger.error("Camera ImagePicker source not available")
threadSafeCallback(false)
return
}
let authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
guard CurrentAppContext().reportedApplicationState != .background else {
Logger.warn("Skipping camera permissions request when app is in background, relying on previous status \(authorizationStatus.rawValue)")
threadSafeCallback(authorizationStatus == .authorized)
return
}
let presentSettingsDialog = {
AssertIsOnMainThread()
let actionSheet = ActionSheetController(
title: OWSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title"),
message: OWSLocalizedString("MISSING_CAMERA_PERMISSION_MESSAGE", comment: "Alert body")
)
if let openSettingsAction = AppContextUtils.openSystemSettingsAction(completion: { threadSafeCallback(false) }) {
actionSheet.addAction(openSettingsAction)
}
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.dismissButton,
style: .cancel,
handler: { _ in
threadSafeCallback(false)
}
))
self.presentActionSheet(actionSheet)
}
switch authorizationStatus {
case .denied:
DispatchMainThreadSafe { presentSettingsDialog() }
case .authorized:
threadSafeCallback(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: threadSafeCallback)
default:
Logger.error("Unknown AVAuthorizationStatus: \(authorizationStatus)")
threadSafeCallback(false)
}
}
public func ows_askForMediaLibraryPermissions(callback: @escaping (Bool) -> Void) {
Logger.verbose("\(String(describing: Self.self)) ows_askForMediaLibraryPermissions")
// Ensure callback is invoked on main thread.
let threadSafeCallback: (Bool) -> Void = { granted in
DispatchMainThreadSafe {
callback(granted)
}
}
guard CurrentAppContext().reportedApplicationState != .background else {
Logger.error("Skipping media library permissions request when app is in background.")
threadSafeCallback(false)
return
}
guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else {
Logger.error("PhotoLibrary ImagePicker source not available")
threadSafeCallback(false)
return
}
let presentSettingsDialog = {
AssertIsOnMainThread()
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"MISSING_MEDIA_LIBRARY_PERMISSION_TITLE",
comment: "Alert title when user has previously denied media library access"
),
message: OWSLocalizedString(
"MISSING_MEDIA_LIBRARY_PERMISSION_MESSAGE",
comment: "Alert body when user has previously denied media library access"
)
)
if let openSettingsAction = AppContextUtils.openSystemSettingsAction(completion: { threadSafeCallback(false) }) {
actionSheet.addAction(openSettingsAction)
}
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.dismissButton,
style: .cancel,
handler: { _ in
threadSafeCallback(false)
}
))
self.presentActionSheet(actionSheet)
}
let authorizationStatus = PHPhotoLibrary.authorizationStatus()
switch authorizationStatus {
case .notDetermined:
PHPhotoLibrary.requestAuthorization { status in
switch status {
case .authorized, .limited:
threadSafeCallback(true)
default:
DispatchMainThreadSafe { presentSettingsDialog() }
}
}
case .denied, .restricted:
DispatchMainThreadSafe { presentSettingsDialog() }
case .authorized, .limited:
threadSafeCallback(true)
@unknown default:
owsFail("Unknown authorization status \(authorizationStatus)")
}
}
public func ows_askForMicrophonePermissions(callback: @escaping (Bool) -> Void) {
Logger.verbose("\(String(describing: Self.self)) ows_askForMicrophonePermissions")
// Ensure callback is invoked on main thread.
let threadSafeCallback: (Bool) -> Void = { granted in
DispatchMainThreadSafe {
callback(granted)
}
}
// We want to avoid asking for audio permission while the app is in the background,
// as WebRTC can ask at some strange times. However, if we're currently in a call
// it's important we allow you to request audio permission regardless of app state.
guard CurrentAppContext().reportedApplicationState != .background || CurrentAppContext().hasActiveCall else {
Logger.error("Skipping microphone permissions request when app is in background.")
threadSafeCallback(false)
return
}
AVAudioSession.sharedInstance().requestRecordPermission(threadSafeCallback)
}
public func ows_showNoMicrophonePermissionActionSheet() {
DispatchMainThreadSafe {
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"CALL_AUDIO_PERMISSION_TITLE",
comment: "Alert title when calling and permissions for microphone are missing"
),
message: OWSLocalizedString(
"CALL_AUDIO_PERMISSION_MESSAGE",
comment: "Alert message when calling and permissions for microphone are missing"
)
)
if let openSettingsAction = AppContextUtils.openSystemSettingsAction() {
actionSheet.addAction(openSettingsAction)
}
actionSheet.addAction(OWSActionSheets.dismissAction)
self.presentActionSheet(actionSheet)
}
}
}