Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "DeviceAccess",
module_name = "DeviceAccess",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/Display:Display",
"//submodules/AccountContext:AccountContext",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/LegacyComponents:LegacyComponents",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,567 @@
import Foundation
import UIKit
import AVFoundation
import Display
import TelegramCore
import SwiftSignalKit
import Photos
import CoreLocation
import Contacts
import UserNotifications
import CoreTelephony
import TelegramPresentationData
import AccountContext
public enum DeviceAccessCameraSubject {
case video
case videoCall
case qrCode
case ageVerification
}
public enum DeviceAccessMicrophoneSubject {
case audio
case video
case voiceCall
}
public enum DeviceAccessMediaLibrarySubject {
case send
case save
case wallpaper
case qrCode
}
public enum DeviceAccessLocationSubject {
case send
case live
case tracking
case weather
}
public enum DeviceAccessSubject {
case camera(DeviceAccessCameraSubject)
case microphone(DeviceAccessMicrophoneSubject)
case mediaLibrary(DeviceAccessMediaLibrarySubject)
case location(DeviceAccessLocationSubject)
case contacts
case notifications
case siri
case cellularData
}
private let cachedMediaLibraryAccessStatus = Atomic<Bool?>(value: nil)
public func shouldDisplayNotificationsPermissionWarning(status: AccessType, suppressed: Bool) -> Bool {
switch (status, suppressed) {
case (.allowed, _), (.unreachable, true), (.notDetermined, true):
return false
default:
return true
}
}
public final class DeviceAccess {
private static let contactsPromise = Promise<Bool?>(nil)
static var contacts: Signal<Bool?, NoError> {
return self.contactsPromise.get()
|> distinctUntilChanged
}
private static let notificationsPromise = Promise<Bool?>(nil)
static var notifications: Signal<Bool?, NoError> {
return self.notificationsPromise.get()
}
private static let siriPromise = Promise<Bool?>(nil)
static var siri: Signal<Bool?, NoError> {
return self.siriPromise.get()
}
private static let locationPromise = Promise<Bool?>(nil)
static var location: Signal<Bool?, NoError> {
return self.locationPromise.get()
}
private static let cameraPromise = Promise<Bool?>(nil)
static var camera: Signal<Bool?, NoError> {
return self.cameraPromise.get()
}
private static let microphonePromise = Promise<Bool?>(nil)
static var microphone: Signal<Bool?, NoError> {
return self.microphonePromise.get()
}
public static func isMicrophoneAccessAuthorized() -> Bool? {
return AVAudioSession.sharedInstance().recordPermission == .granted
}
public static func isCameraAccessAuthorized() -> Bool {
return AVCaptureDevice.authorizationStatus(for: .video) == .authorized
}
public static func authorizationStatus(applicationInForeground: Signal<Bool, NoError>? = nil, siriAuthorization: (() -> AccessType)? = nil, subject: DeviceAccessSubject) -> Signal<AccessType, NoError> {
switch subject {
case .notifications:
let status = (Signal<AccessType, NoError> { subscriber in
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
UNUserNotificationCenter.current().getNotificationSettings(completionHandler: { settings in
switch settings.authorizationStatus {
case .authorized:
if settings.alertSetting == .disabled {
subscriber.putNext(.unreachable)
} else {
subscriber.putNext(.allowed)
}
case .denied:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
default:
subscriber.putNext(.notDetermined)
}
subscriber.putCompletion()
})
} else {
subscriber.putNext(.notDetermined)
subscriber.putCompletion()
}
return EmptyDisposable
} |> afterNext { status in
switch status {
case .allowed, .unreachable:
DeviceAccess.notificationsPromise.set(.single(nil))
default:
break
}
} )
|> then(self.notifications
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
})
if let applicationInForeground = applicationInForeground {
return applicationInForeground
|> distinctUntilChanged
|> mapToSignal { inForeground -> Signal<AccessType, NoError> in
return status
}
} else {
return status
}
case .contacts:
let status = Signal<AccessType, NoError> { subscriber in
switch CNContactStore.authorizationStatus(for: .contacts) {
case .notDetermined:
subscriber.putNext(.notDetermined)
case .authorized:
subscriber.putNext(.allowed)
case .limited:
subscriber.putNext(.limited)
default:
subscriber.putNext(.denied)
}
subscriber.putCompletion()
return EmptyDisposable
}
return status
|> then(self.contacts
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
})
case .cellularData:
return Signal { subscriber in
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
func statusForCellularState(_ state: CTCellularDataRestrictedState) -> AccessType? {
switch state {
case .restricted:
return .denied
case .notRestricted:
return .allowed
default:
return .allowed
}
}
let cellState = CTCellularData.init()
if let status = statusForCellularState(cellState.restrictedState) {
subscriber.putNext(status)
}
cellState.cellularDataRestrictionDidUpdateNotifier = { restrictedState in
if let status = statusForCellularState(restrictedState) {
subscriber.putNext(status)
}
}
} else {
subscriber.putNext(.allowed)
subscriber.putCompletion()
}
return EmptyDisposable
}
case .siri:
if let siriAuthorization = siriAuthorization {
return Signal { subscriber in
let status = siriAuthorization()
subscriber.putNext(status)
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.siri
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
})
} else {
return .single(.denied)
}
case .location:
return Signal { subscriber in
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways, .authorizedWhenInUse:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.location
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
}
)
case .camera:
return Signal { subscriber in
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.camera
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
}
)
case .microphone:
return Signal { subscriber in
let status = AVCaptureDevice.authorizationStatus(for: .audio)
switch status {
case .authorized:
subscriber.putNext(.allowed)
case .denied, .restricted:
subscriber.putNext(.denied)
case .notDetermined:
subscriber.putNext(.notDetermined)
@unknown default:
fatalError()
}
subscriber.putCompletion()
return EmptyDisposable
}
|> then(self.microphone
|> mapToSignal { authorized -> Signal<AccessType, NoError> in
if let authorized = authorized {
return .single(authorized ? .allowed : .denied)
} else {
return .complete()
}
}
)
default:
return .single(.notDetermined)
}
}
public static func authorizeAccess(
to subject: DeviceAccessSubject,
onlyCheck: Bool = false,
registerForNotifications: ((@escaping (Bool) -> Void) -> Void)? = nil,
requestSiriAuthorization: ((@escaping (Bool) -> Void) -> Void)? = nil,
locationManager: LocationManager? = nil,
presentationData: PresentationData? = nil,
present: @escaping (ViewController, Any?) -> Void = { _, _ in },
openSettings: @escaping () -> Void = { },
displayNotificationFromBackground: @escaping (String) -> Void = { _ in },
_ completion: @escaping (Bool) -> Void = { _ in }) {
switch subject {
case let .camera(cameraSubject):
let status = AVCaptureDevice.authorizationStatus(for: .video)
if case .notDetermined = status {
if !onlyCheck {
AVCaptureDevice.requestAccess(for: AVMediaType.video) { response in
Queue.mainQueue().async {
completion(response)
self.cameraPromise.set(.single(response))
if !response, let presentationData = presentationData {
let text: String
switch cameraSubject {
case .video:
text = presentationData.strings.AccessDenied_Camera
case .videoCall:
text = presentationData.strings.AccessDenied_VideoCallCamera
case .qrCode:
text = presentationData.strings.AccessDenied_QrCamera
case .ageVerification:
text = presentationData.strings.AccessDenied_AgeVerificationCamera
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
}
}
} else {
completion(true)
}
} else if [.restricted, .denied].contains(status) {
completion(false)
if let presentationData = presentationData {
let text: String
if case .restricted = status {
text = presentationData.strings.AccessDenied_CameraRestricted
} else {
switch cameraSubject {
case .video:
text = presentationData.strings.AccessDenied_Camera
case .videoCall:
text = presentationData.strings.AccessDenied_VideoCallCamera
case .qrCode:
text = presentationData.strings.AccessDenied_QrCamera
case .ageVerification:
text = presentationData.strings.AccessDenied_AgeVerificationCamera
}
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
} else if case .authorized = status {
completion(true)
} else {
assertionFailure()
completion(true)
}
case let .microphone(microphoneSubject):
if AVAudioSession.sharedInstance().recordPermission == .granted {
completion(true)
} else {
AVAudioSession.sharedInstance().requestRecordPermission({ granted in
Queue.mainQueue().async {
if granted {
completion(true)
} else if let presentationData = presentationData {
completion(false)
let text: String
switch microphoneSubject {
case .audio:
text = presentationData.strings.AccessDenied_VoiceMicrophone
case .video:
text = presentationData.strings.AccessDenied_VideoMicrophone
case .voiceCall:
text = presentationData.strings.AccessDenied_CallMicrophone
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
if case .voiceCall = microphoneSubject {
displayNotificationFromBackground(text)
}
}
self.microphonePromise.set(.single(granted))
}
})
}
case let .mediaLibrary(mediaLibrarySubject):
let continueWithValue: (Bool) -> Void = { value in
Queue.mainQueue().async {
if value {
completion(true)
} else if let presentationData = presentationData {
completion(false)
let text: String
switch mediaLibrarySubject {
case .send:
text = presentationData.strings.AccessDenied_PhotosAndVideos
case .save:
text = presentationData.strings.AccessDenied_SaveMedia
case .wallpaper:
text = presentationData.strings.AccessDenied_Wallpapers
case .qrCode:
text = presentationData.strings.AccessDenied_QrCode
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
}
}
if let value = cachedMediaLibraryAccessStatus.with({ $0 }) {
continueWithValue(value)
} else {
PHPhotoLibrary.requestAuthorization({ status in
let value: Bool
switch status {
case .restricted, .denied, .notDetermined:
value = false
case .authorized, .limited:
value = true
@unknown default:
fatalError()
}
let _ = cachedMediaLibraryAccessStatus.swap(value)
continueWithValue(value)
})
}
case let .location(locationSubject):
let status = CLLocationManager.authorizationStatus()
let hasPreciseLocation: Bool
if #available(iOS 14.0, *) {
if case .fullAccuracy = CLLocationManager().accuracyAuthorization {
hasPreciseLocation = true
} else {
hasPreciseLocation = false
}
} else {
hasPreciseLocation = true
}
switch status {
case .authorizedAlways:
if case .live = locationSubject, !hasPreciseLocation {
completion(false)
if let presentationData = presentationData {
let text = presentationData.strings.AccessDenied_LocationPreciseDenied
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
} else {
completion(true)
}
case .authorizedWhenInUse:
switch locationSubject {
case .send, .tracking, .weather:
completion(true)
case .live:
completion(false)
if let presentationData = presentationData {
let text = presentationData.strings.AccessDenied_LocationAlwaysDenied
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
}
case .denied, .restricted:
completion(false)
if let presentationData = presentationData {
let text: String
if status == .denied {
switch locationSubject {
case .send, .live:
text = presentationData.strings.AccessDenied_LocationDenied
case .tracking:
text = presentationData.strings.AccessDenied_LocationTracking
case .weather:
text = presentationData.strings.AccessDenied_LocationWeather
}
} else {
text = presentationData.strings.AccessDenied_LocationDisabled
}
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.AccessDenied_Title, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
case .notDetermined:
switch locationSubject {
case .send, .tracking, .weather:
locationManager?.requestWhenInUseAuthorization(completion: { status in
completion(status == .authorizedWhenInUse || status == .authorizedAlways)
})
case .live:
locationManager?.requestAlwaysAuthorization(completion: { status in
completion(status == .authorizedAlways)
})
}
@unknown default:
fatalError()
}
case .contacts:
let _ = (self.contactsPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { value in
if let value = value {
completion(value)
} else {
switch CNContactStore.authorizationStatus(for: .contacts) {
case .notDetermined:
let store = CNContactStore()
store.requestAccess(for: .contacts, completionHandler: { authorized, _ in
self.contactsPromise.set(.single(authorized))
completion(authorized)
})
case .authorized:
self.contactsPromise.set(.single(true))
completion(true)
case .limited:
self.contactsPromise.set(.single(true))
completion(true)
default:
self.contactsPromise.set(.single(false))
completion(false)
}
}
})
case .notifications:
if let registerForNotifications = registerForNotifications {
registerForNotifications { result in
self.notificationsPromise.set(.single(result))
completion(result)
}
}
case .siri:
if let requestSiriAuthorization = requestSiriAuthorization {
requestSiriAuthorization { result in
self.siriPromise.set(.single(result))
completion(result)
}
}
case .cellularData:
if let presentationData = presentationData {
present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: presentationData.strings.Permissions_CellularDataTitle_v0, text: presentationData.strings.Permissions_CellularDataText_v0, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
openSettings()
})]), nil)
}
}
}
}
@@ -0,0 +1,39 @@
import Foundation
import CoreLocation
public final class LocationManager: NSObject, CLLocationManagerDelegate {
public let manager = CLLocationManager()
var pendingCompletion: ((CLAuthorizationStatus) -> Void, CLAuthorizationStatus)?
public override init() {
super.init()
self.manager.delegate = self
}
func requestWhenInUseAuthorization(completion: @escaping (CLAuthorizationStatus) -> Void) {
let status = CLLocationManager.authorizationStatus()
if status == .notDetermined {
self.manager.requestWhenInUseAuthorization()
self.pendingCompletion = (completion, .authorizedWhenInUse)
} else {
completion(status)
}
}
func requestAlwaysAuthorization(completion: @escaping (CLAuthorizationStatus) -> Void) {
let status = CLLocationManager.authorizationStatus()
if status == .notDetermined {
self.manager.requestWhenInUseAuthorization()
self.pendingCompletion = (completion, .authorizedAlways)
} else {
completion(status)
}
}
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if let (pendingCompletion, _) = self.pendingCompletion {
pendingCompletion(status)
self.pendingCompletion = nil
}
}
}