import { AVCaptureDevice, AVMediaType, AVAuthorizationStatus } from 'AVFoundation' import { AVAudioSession } from 'AVFoundation' import { PHPhotoLibrary, PHAuthorizationStatus } from 'Photos' import { CLLocationManager, CLAuthorizationStatus } from 'CoreLocation' import { CNContactStore, CNAuthorizationStatus, CNEntityType } from 'Contacts' import { CBManager, CBManagerAuthorization, CBCentralManager, CBManagerState } from 'CoreBluetooth' import { UIApplication } from 'UIKit' import { DispatchQueue } from 'Dispatch' import { NSError, URL } from 'Foundation' import { BluetoothDemoInfo, CheckPermission, CheckPermissionOptions, CheckPermissions, CheckPermissionsOptions, CheckPermissionsResult, CheckPermissionsSync, CheckPermissionSync, GetSupportedPermissionsResult, GetSupportedPermissionsSync, GetBluetoothDemoInfoSync, OpenAppSettings, OpenAppSettingsOptions, PermissionCheckResult, PermissionKey, PermissionGrantResult, PermissionStatus, RequestPermission, RequestPermissionOptions, RequestPermissions, RequestPermissionsOptions, RequestPermissionsResult, } from '../interface.uts' import { PermissionFailImpl } from '../unierror' const SUPPORTED_PERMISSIONS : PermissionKey[] = [ 'camera', 'photoLibrary', 'storage', 'readStorage', 'location', 'coarseLocation', 'microphone', 'contacts', 'writeContacts', 'bluetooth', ] let locationManager : CLLocationManager | null = null let pendingLocationOptions : RequestPermissionOptions | null = null function createCheckResult(permission : PermissionKey, status : PermissionStatus) : PermissionCheckResult { return { permission: permission, status: status, granted: status == 'granted', } } function createRequestResult(permission : PermissionKey, status : PermissionStatus) : PermissionGrantResult { return { permission: permission, status: status, granted: status == 'granted', } } function isIosUnsupported(permission : PermissionKey) : boolean { return permission == 'phone' || permission == 'phoneState' || permission == 'sms' || permission == 'readSms' || permission == 'receiveSms' || permission == 'overlay' || permission == 'writeSettings' } function mapPhotoStatus(status : PHAuthorizationStatus) : PermissionStatus { if (status == PHAuthorizationStatus.authorized || status == PHAuthorizationStatus.limited) { return 'granted' } if (status == PHAuthorizationStatus.notDetermined) { return 'notDetermined' } if (status == PHAuthorizationStatus.restricted) { return 'restricted' } return 'denied' } function mapLocationStatus(status : CLAuthorizationStatus) : PermissionStatus { if (status == CLAuthorizationStatus.authorizedWhenInUse || status == CLAuthorizationStatus.authorizedAlways) { return 'granted' } if (status == CLAuthorizationStatus.notDetermined) { return 'notDetermined' } if (status == CLAuthorizationStatus.restricted) { return 'restricted' } return 'denied' } function mapContactStatus(status : CNAuthorizationStatus) : PermissionStatus { if (status == CNAuthorizationStatus.authorized) { return 'granted' } if (status == CNAuthorizationStatus.notDetermined) { return 'notDetermined' } if (status == CNAuthorizationStatus.restricted) { return 'restricted' } return 'denied' } function mapBluetoothStatus(status : CBManagerAuthorization) : PermissionStatus { if (status == CBManagerAuthorization.allowedAlways) { return 'granted' } if (status == CBManagerAuthorization.notDetermined) { return 'notDetermined' } if (status == CBManagerAuthorization.restricted) { return 'restricted' } return 'denied' } function resolveCameraPermissionStatus() : PermissionStatus { const status = AVCaptureDevice.authorizationStatus(for = AVMediaType.video) if (status == AVAuthorizationStatus.authorized) { return 'granted' } if (status == AVAuthorizationStatus.notDetermined) { return 'notDetermined' } if (status == AVAuthorizationStatus.restricted) { return 'restricted' } return 'denied' } function resolveMicrophonePermissionStatus() : PermissionStatus { const recordPermission = AVAudioSession.sharedInstance().recordPermission if (recordPermission == AVAudioSession.RecordPermission.granted) { return 'granted' } if (recordPermission == AVAudioSession.RecordPermission.undetermined) { return 'notDetermined' } return 'denied' } function resolvePermissionStatus(permission : PermissionKey) : PermissionStatus { if (isIosUnsupported(permission)) { return 'unsupported' } if (permission == 'camera') { return resolveCameraPermissionStatus() } if (permission == 'photoLibrary' || permission == 'storage' || permission == 'readStorage') { return mapPhotoStatus(PHPhotoLibrary.authorizationStatus()) } if (permission == 'location' || permission == 'coarseLocation') { return mapLocationStatus(CLLocationManager.authorizationStatus()) } if (permission == 'microphone') { return resolveMicrophonePermissionStatus() } if (permission == 'contacts' || permission == 'writeContacts') { return mapContactStatus(CNContactStore.authorizationStatus(for = CNEntityType.contacts)) } if (permission == 'bluetooth') { return mapBluetoothStatus(CBManager.authorization) } return 'unsupported' } function succeedCheckPermission(options : CheckPermissionOptions, res : PermissionCheckResult) { const successCallback = options.success if (successCallback != null) { successCallback(res) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(res) } } function succeedCheckPermissions(options : CheckPermissionsOptions, res : CheckPermissionsResult) { const successCallback = options.success if (successCallback != null) { successCallback(res) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(res) } } function succeedRequestPermission(options : RequestPermissionOptions, res : PermissionGrantResult) { const successCallback = options.success if (successCallback != null) { successCallback(res) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(res) } } function succeedRequestPermissions(options : RequestPermissionsOptions, res : RequestPermissionsResult) { const successCallback = options.success if (successCallback != null) { successCallback(res) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(res) } } function succeedOpenAppSettings(options : OpenAppSettingsOptions, res : PermissionCheckResult) { const successCallback = options.success if (successCallback != null) { successCallback(res) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(res) } } function failRequestPermission(options : RequestPermissionOptions, err : PermissionFailImpl) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFailImpl) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFailImpl) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function requestCameraPermission(options : RequestPermissionOptions) { AVCaptureDevice.requestAccess(for = AVMediaType.video, completionHandler = (granted : boolean) => { DispatchQueue.main.async(execute = (): void => { const status : PermissionStatus = granted ? 'granted' : 'denied' succeedRequestPermission(options, createRequestResult(options.permission, status)) }) }) } function requestPhotoPermission(options : RequestPermissionOptions) { PHPhotoLibrary.requestAuthorization((status : PHAuthorizationStatus) => { DispatchQueue.main.async(execute = (): void => { succeedRequestPermission(options, createRequestResult(options.permission, mapPhotoStatus(status))) }) }) } function requestMicrophonePermission(options : RequestPermissionOptions) { AVAudioSession.sharedInstance().requestRecordPermission((granted : boolean) => { DispatchQueue.main.async(execute = (): void => { const status : PermissionStatus = granted ? 'granted' : 'denied' succeedRequestPermission(options, createRequestResult(options.permission, status)) }) }) } function requestContactsPermission(options : RequestPermissionOptions) { const store = new CNContactStore() store.requestAccess(for = CNEntityType.contacts, completionHandler = (granted : boolean, _ : NSError | null) => { DispatchQueue.main.async(execute = (): void => { const status : PermissionStatus = granted ? 'granted' : 'denied' succeedRequestPermission(options, createRequestResult(options.permission, status)) }) }) } function ensureLocationManager() : CLLocationManager { if (locationManager == null) { locationManager = new CLLocationManager() } return locationManager! } function requestLocationPermission(options : RequestPermissionOptions) { const currentStatus = CLLocationManager.authorizationStatus() if (currentStatus != CLAuthorizationStatus.notDetermined) { succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(currentStatus))) return } pendingLocationOptions = options const manager = ensureLocationManager() manager.requestWhenInUseAuthorization() scheduleLocationPermissionCheck(0) } function scheduleLocationPermissionCheck(attempt : Int) { const maxAttempts : Int = 60 DispatchQueue.main.asyncAfter(deadline = DispatchTime.now() + 0.3, execute = (): void => { const options = pendingLocationOptions if (options == null) { return } const authStatus = CLLocationManager.authorizationStatus() if (authStatus == CLAuthorizationStatus.notDetermined) { if (attempt >= maxAttempts) { pendingLocationOptions = null succeedRequestPermission(options, createRequestResult(options.permission, 'denied')) return } scheduleLocationPermissionCheck(attempt + 1) return } pendingLocationOptions = null succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(authStatus))) }) } function requestBluetoothPermission(options : RequestPermissionOptions) { const currentStatus = CBManager.authorization if (currentStatus != CBManagerAuthorization.notDetermined) { succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(currentStatus))) return } // 创建 CBCentralManager 实例会触发系统蓝牙权限弹窗 const _centralManager = new CBCentralManager() DispatchQueue.main.async(execute = (): void => { succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(CBManager.authorization))) }) } export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult { return createCheckResult(permission, resolvePermissionStatus(permission)) } export const checkPermissionsSync : CheckPermissionsSync = function (permissions : PermissionKey[]) : CheckPermissionsResult { const results : PermissionCheckResult[] = [] let allGranted = true for (let i = 0; i < permissions.length; i++) { const item = checkPermissionSync(permissions[i]) results.push(item) if (!item.granted) { allGranted = false } } return { results: results, allGranted: allGranted, } } export const checkPermission : CheckPermission = function (options : CheckPermissionOptions) { succeedCheckPermission(options, checkPermissionSync(options.permission)) } export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) { succeedCheckPermissions(options, checkPermissionsSync(options.permissions)) } export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) { if (isIosUnsupported(options.permission)) { failRequestPermission(options, new PermissionFailImpl(9020003)) return } const currentStatus = resolvePermissionStatus(options.permission) if (currentStatus == 'granted') { succeedRequestPermission(options, createRequestResult(options.permission, 'granted')) return } if (currentStatus == 'restricted') { succeedRequestPermission(options, createRequestResult(options.permission, 'restricted')) return } switch (options.permission) { case 'camera': requestCameraPermission(options) break case 'photoLibrary': case 'storage': case 'readStorage': requestPhotoPermission(options) break case 'location': case 'coarseLocation': requestLocationPermission(options) break case 'microphone': requestMicrophonePermission(options) break case 'contacts': case 'writeContacts': requestContactsPermission(options) break case 'bluetooth': requestBluetoothPermission(options) break default: failRequestPermission(options, new PermissionFailImpl(9020003)) } } export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) { const permissions = options.permissions if (permissions == null) { const emptyResult : RequestPermissionsResult = { results: [], allGranted: true, } succeedRequestPermissions(options, emptyResult) return } const permissionList = permissions! if (permissionList.length == 0) { const emptyResult : RequestPermissionsResult = { results: [], allGranted: true, } succeedRequestPermissions(options, emptyResult) return } let currentIndex = 0 const collectedResults : PermissionGrantResult[] = [] function requestNext() { if (currentIndex >= permissionList.length) { let allGranted = true for (let i = 0; i < collectedResults.length; i++) { if (!collectedResults[i].granted) { allGranted = false break } } const finalResult : RequestPermissionsResult = { results: collectedResults, allGranted: allGranted, } succeedRequestPermissions(options, finalResult) return } const permission = permissionList[currentIndex] currentIndex += 1 requestPermission({ permission: permission, success: (res : PermissionGrantResult) => { collectedResults.push(res) requestNext() }, fail: (err : PermissionFailImpl) => { collectedResults.push(createRequestResult(permission, 'denied')) failRequestPermissions(options, err) }, }) } requestNext() } export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) { const settingsUrl = URL(string = UIApplication.openSettingsURLString) if (settingsUrl == null) { failOpenAppSettings(options, new PermissionFailImpl(9020004)) return } DispatchQueue.main.async(execute = (): void => { UIApplication.shared.open(settingsUrl!) const resultPermission = options.permission != null ? options.permission! : 'camera' succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted')) }) } export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult { return { permissions: SUPPORTED_PERMISSIONS, } } function mapIOSBluetoothState(state : CBManagerState) : string { switch (state) { case CBManagerState.poweredOn: return '已开启' case CBManagerState.poweredOff: return '已关闭' case CBManagerState.unsupported: return '不支持' case CBManagerState.unauthorized: return '未授权' case CBManagerState.resetting: return '重置中' case CBManagerState.unknown: return '未知' default: return '未知' } } export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo { const manager = new CBCentralManager() const state = manager.state return { available: state != CBManagerState.unsupported, enabled: state == CBManagerState.poweredOn, stateText: mapIOSBluetoothState(state), stateCode: state as number, } }