index.uts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. import { AVCaptureDevice, AVMediaType, AVAuthorizationStatus } from 'AVFoundation'
  2. import { AVAudioSession } from 'AVFoundation'
  3. import { PHPhotoLibrary, PHAuthorizationStatus } from 'Photos'
  4. import { CLLocationManager, CLAuthorizationStatus } from 'CoreLocation'
  5. import { CNContactStore, CNAuthorizationStatus, CNEntityType } from 'Contacts'
  6. import { CBManager, CBManagerAuthorization, CBCentralManager, CBManagerState } from 'CoreBluetooth'
  7. import { UIApplication } from 'UIKit'
  8. import { DispatchQueue } from 'Dispatch'
  9. import { NSError, URL } from 'Foundation'
  10. import {
  11. BluetoothDemoInfo,
  12. CheckPermission,
  13. CheckPermissionOptions,
  14. CheckPermissions,
  15. CheckPermissionsOptions,
  16. CheckPermissionsResult,
  17. CheckPermissionsSync,
  18. CheckPermissionSync,
  19. GetSupportedPermissionsResult,
  20. GetSupportedPermissionsSync,
  21. GetBluetoothDemoInfoSync,
  22. OpenAppSettings,
  23. OpenAppSettingsOptions,
  24. PermissionCheckResult,
  25. PermissionKey,
  26. PermissionGrantResult,
  27. PermissionStatus,
  28. RequestPermission,
  29. RequestPermissionOptions,
  30. RequestPermissions,
  31. RequestPermissionsOptions,
  32. RequestPermissionsResult,
  33. } from '../interface.uts'
  34. import { PermissionFailImpl } from '../unierror'
  35. const SUPPORTED_PERMISSIONS : PermissionKey[] = [
  36. 'camera',
  37. 'photoLibrary',
  38. 'storage',
  39. 'readStorage',
  40. 'location',
  41. 'coarseLocation',
  42. 'microphone',
  43. 'contacts',
  44. 'writeContacts',
  45. 'bluetooth',
  46. ]
  47. let locationManager : CLLocationManager | null = null
  48. let pendingLocationOptions : RequestPermissionOptions | null = null
  49. function createCheckResult(permission : PermissionKey, status : PermissionStatus) : PermissionCheckResult {
  50. return {
  51. permission: permission,
  52. status: status,
  53. granted: status == 'granted',
  54. }
  55. }
  56. function createRequestResult(permission : PermissionKey, status : PermissionStatus) : PermissionGrantResult {
  57. return {
  58. permission: permission,
  59. status: status,
  60. granted: status == 'granted',
  61. }
  62. }
  63. function isIosUnsupported(permission : PermissionKey) : boolean {
  64. return permission == 'phone'
  65. || permission == 'phoneState'
  66. || permission == 'sms'
  67. || permission == 'readSms'
  68. || permission == 'receiveSms'
  69. || permission == 'overlay'
  70. || permission == 'writeSettings'
  71. }
  72. function mapPhotoStatus(status : PHAuthorizationStatus) : PermissionStatus {
  73. if (status == PHAuthorizationStatus.authorized || status == PHAuthorizationStatus.limited) {
  74. return 'granted'
  75. }
  76. if (status == PHAuthorizationStatus.notDetermined) {
  77. return 'notDetermined'
  78. }
  79. if (status == PHAuthorizationStatus.restricted) {
  80. return 'restricted'
  81. }
  82. return 'denied'
  83. }
  84. function mapLocationStatus(status : CLAuthorizationStatus) : PermissionStatus {
  85. if (status == CLAuthorizationStatus.authorizedWhenInUse || status == CLAuthorizationStatus.authorizedAlways) {
  86. return 'granted'
  87. }
  88. if (status == CLAuthorizationStatus.notDetermined) {
  89. return 'notDetermined'
  90. }
  91. if (status == CLAuthorizationStatus.restricted) {
  92. return 'restricted'
  93. }
  94. return 'denied'
  95. }
  96. function mapContactStatus(status : CNAuthorizationStatus) : PermissionStatus {
  97. if (status == CNAuthorizationStatus.authorized) {
  98. return 'granted'
  99. }
  100. if (status == CNAuthorizationStatus.notDetermined) {
  101. return 'notDetermined'
  102. }
  103. if (status == CNAuthorizationStatus.restricted) {
  104. return 'restricted'
  105. }
  106. return 'denied'
  107. }
  108. function mapBluetoothStatus(status : CBManagerAuthorization) : PermissionStatus {
  109. if (status == CBManagerAuthorization.allowedAlways) {
  110. return 'granted'
  111. }
  112. if (status == CBManagerAuthorization.notDetermined) {
  113. return 'notDetermined'
  114. }
  115. if (status == CBManagerAuthorization.restricted) {
  116. return 'restricted'
  117. }
  118. return 'denied'
  119. }
  120. function resolveCameraPermissionStatus() : PermissionStatus {
  121. const status = AVCaptureDevice.authorizationStatus(for = AVMediaType.video)
  122. if (status == AVAuthorizationStatus.authorized) {
  123. return 'granted'
  124. }
  125. if (status == AVAuthorizationStatus.notDetermined) {
  126. return 'notDetermined'
  127. }
  128. if (status == AVAuthorizationStatus.restricted) {
  129. return 'restricted'
  130. }
  131. return 'denied'
  132. }
  133. function resolveMicrophonePermissionStatus() : PermissionStatus {
  134. const recordPermission = AVAudioSession.sharedInstance().recordPermission
  135. if (recordPermission == AVAudioSession.RecordPermission.granted) {
  136. return 'granted'
  137. }
  138. if (recordPermission == AVAudioSession.RecordPermission.undetermined) {
  139. return 'notDetermined'
  140. }
  141. return 'denied'
  142. }
  143. function resolvePermissionStatus(permission : PermissionKey) : PermissionStatus {
  144. if (isIosUnsupported(permission)) {
  145. return 'unsupported'
  146. }
  147. if (permission == 'camera') {
  148. return resolveCameraPermissionStatus()
  149. }
  150. if (permission == 'photoLibrary' || permission == 'storage' || permission == 'readStorage') {
  151. return mapPhotoStatus(PHPhotoLibrary.authorizationStatus())
  152. }
  153. if (permission == 'location' || permission == 'coarseLocation') {
  154. return mapLocationStatus(CLLocationManager.authorizationStatus())
  155. }
  156. if (permission == 'microphone') {
  157. return resolveMicrophonePermissionStatus()
  158. }
  159. if (permission == 'contacts' || permission == 'writeContacts') {
  160. return mapContactStatus(CNContactStore.authorizationStatus(for = CNEntityType.contacts))
  161. }
  162. if (permission == 'bluetooth') {
  163. return mapBluetoothStatus(CBManager.authorization)
  164. }
  165. return 'unsupported'
  166. }
  167. function succeedCheckPermission(options : CheckPermissionOptions, res : PermissionCheckResult) {
  168. const successCallback = options.success
  169. if (successCallback != null) {
  170. successCallback(res)
  171. }
  172. const completeCallback = options.complete
  173. if (completeCallback != null) {
  174. completeCallback(res)
  175. }
  176. }
  177. function succeedCheckPermissions(options : CheckPermissionsOptions, res : CheckPermissionsResult) {
  178. const successCallback = options.success
  179. if (successCallback != null) {
  180. successCallback(res)
  181. }
  182. const completeCallback = options.complete
  183. if (completeCallback != null) {
  184. completeCallback(res)
  185. }
  186. }
  187. function succeedRequestPermission(options : RequestPermissionOptions, res : PermissionGrantResult) {
  188. const successCallback = options.success
  189. if (successCallback != null) {
  190. successCallback(res)
  191. }
  192. const completeCallback = options.complete
  193. if (completeCallback != null) {
  194. completeCallback(res)
  195. }
  196. }
  197. function succeedRequestPermissions(options : RequestPermissionsOptions, res : RequestPermissionsResult) {
  198. const successCallback = options.success
  199. if (successCallback != null) {
  200. successCallback(res)
  201. }
  202. const completeCallback = options.complete
  203. if (completeCallback != null) {
  204. completeCallback(res)
  205. }
  206. }
  207. function succeedOpenAppSettings(options : OpenAppSettingsOptions, res : PermissionCheckResult) {
  208. const successCallback = options.success
  209. if (successCallback != null) {
  210. successCallback(res)
  211. }
  212. const completeCallback = options.complete
  213. if (completeCallback != null) {
  214. completeCallback(res)
  215. }
  216. }
  217. function failRequestPermission(options : RequestPermissionOptions, err : PermissionFailImpl) {
  218. const failCallback = options.fail
  219. if (failCallback != null) {
  220. failCallback(err)
  221. }
  222. const completeCallback = options.complete
  223. if (completeCallback != null) {
  224. completeCallback(err)
  225. }
  226. }
  227. function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFailImpl) {
  228. const failCallback = options.fail
  229. if (failCallback != null) {
  230. failCallback(err)
  231. }
  232. const completeCallback = options.complete
  233. if (completeCallback != null) {
  234. completeCallback(err)
  235. }
  236. }
  237. function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFailImpl) {
  238. const failCallback = options.fail
  239. if (failCallback != null) {
  240. failCallback(err)
  241. }
  242. const completeCallback = options.complete
  243. if (completeCallback != null) {
  244. completeCallback(err)
  245. }
  246. }
  247. function requestCameraPermission(options : RequestPermissionOptions) {
  248. AVCaptureDevice.requestAccess(for = AVMediaType.video, completionHandler = (granted : boolean) => {
  249. DispatchQueue.main.async(execute = (): void => {
  250. const status : PermissionStatus = granted ? 'granted' : 'denied'
  251. succeedRequestPermission(options, createRequestResult(options.permission, status))
  252. })
  253. })
  254. }
  255. function requestPhotoPermission(options : RequestPermissionOptions) {
  256. PHPhotoLibrary.requestAuthorization((status : PHAuthorizationStatus) => {
  257. DispatchQueue.main.async(execute = (): void => {
  258. succeedRequestPermission(options, createRequestResult(options.permission, mapPhotoStatus(status)))
  259. })
  260. })
  261. }
  262. function requestMicrophonePermission(options : RequestPermissionOptions) {
  263. AVAudioSession.sharedInstance().requestRecordPermission((granted : boolean) => {
  264. DispatchQueue.main.async(execute = (): void => {
  265. const status : PermissionStatus = granted ? 'granted' : 'denied'
  266. succeedRequestPermission(options, createRequestResult(options.permission, status))
  267. })
  268. })
  269. }
  270. function requestContactsPermission(options : RequestPermissionOptions) {
  271. const store = new CNContactStore()
  272. store.requestAccess(for = CNEntityType.contacts, completionHandler = (granted : boolean, _ : NSError | null) => {
  273. DispatchQueue.main.async(execute = (): void => {
  274. const status : PermissionStatus = granted ? 'granted' : 'denied'
  275. succeedRequestPermission(options, createRequestResult(options.permission, status))
  276. })
  277. })
  278. }
  279. function ensureLocationManager() : CLLocationManager {
  280. if (locationManager == null) {
  281. locationManager = new CLLocationManager()
  282. }
  283. return locationManager!
  284. }
  285. function requestLocationPermission(options : RequestPermissionOptions) {
  286. const currentStatus = CLLocationManager.authorizationStatus()
  287. if (currentStatus != CLAuthorizationStatus.notDetermined) {
  288. succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(currentStatus)))
  289. return
  290. }
  291. pendingLocationOptions = options
  292. const manager = ensureLocationManager()
  293. manager.requestWhenInUseAuthorization()
  294. scheduleLocationPermissionCheck(0)
  295. }
  296. function scheduleLocationPermissionCheck(attempt : Int) {
  297. const maxAttempts : Int = 60
  298. DispatchQueue.main.asyncAfter(deadline = DispatchTime.now() + 0.3, execute = (): void => {
  299. const options = pendingLocationOptions
  300. if (options == null) {
  301. return
  302. }
  303. const authStatus = CLLocationManager.authorizationStatus()
  304. if (authStatus == CLAuthorizationStatus.notDetermined) {
  305. if (attempt >= maxAttempts) {
  306. pendingLocationOptions = null
  307. succeedRequestPermission(options, createRequestResult(options.permission, 'denied'))
  308. return
  309. }
  310. scheduleLocationPermissionCheck(attempt + 1)
  311. return
  312. }
  313. pendingLocationOptions = null
  314. succeedRequestPermission(options, createRequestResult(options.permission, mapLocationStatus(authStatus)))
  315. })
  316. }
  317. function requestBluetoothPermission(options : RequestPermissionOptions) {
  318. const currentStatus = CBManager.authorization
  319. if (currentStatus != CBManagerAuthorization.notDetermined) {
  320. succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(currentStatus)))
  321. return
  322. }
  323. // 创建 CBCentralManager 实例会触发系统蓝牙权限弹窗
  324. const _centralManager = new CBCentralManager()
  325. DispatchQueue.main.async(execute = (): void => {
  326. succeedRequestPermission(options, createRequestResult(options.permission, mapBluetoothStatus(CBManager.authorization)))
  327. })
  328. }
  329. export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult {
  330. return createCheckResult(permission, resolvePermissionStatus(permission))
  331. }
  332. export const checkPermissionsSync : CheckPermissionsSync = function (permissions : PermissionKey[]) : CheckPermissionsResult {
  333. const results : PermissionCheckResult[] = []
  334. let allGranted = true
  335. for (let i = 0; i < permissions.length; i++) {
  336. const item = checkPermissionSync(permissions[i])
  337. results.push(item)
  338. if (!item.granted) {
  339. allGranted = false
  340. }
  341. }
  342. return {
  343. results: results,
  344. allGranted: allGranted,
  345. }
  346. }
  347. export const checkPermission : CheckPermission = function (options : CheckPermissionOptions) {
  348. succeedCheckPermission(options, checkPermissionSync(options.permission))
  349. }
  350. export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) {
  351. succeedCheckPermissions(options, checkPermissionsSync(options.permissions))
  352. }
  353. export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) {
  354. if (isIosUnsupported(options.permission)) {
  355. failRequestPermission(options, new PermissionFailImpl(9020003))
  356. return
  357. }
  358. const currentStatus = resolvePermissionStatus(options.permission)
  359. if (currentStatus == 'granted') {
  360. succeedRequestPermission(options, createRequestResult(options.permission, 'granted'))
  361. return
  362. }
  363. if (currentStatus == 'restricted') {
  364. succeedRequestPermission(options, createRequestResult(options.permission, 'restricted'))
  365. return
  366. }
  367. switch (options.permission) {
  368. case 'camera':
  369. requestCameraPermission(options)
  370. break
  371. case 'photoLibrary':
  372. case 'storage':
  373. case 'readStorage':
  374. requestPhotoPermission(options)
  375. break
  376. case 'location':
  377. case 'coarseLocation':
  378. requestLocationPermission(options)
  379. break
  380. case 'microphone':
  381. requestMicrophonePermission(options)
  382. break
  383. case 'contacts':
  384. case 'writeContacts':
  385. requestContactsPermission(options)
  386. break
  387. case 'bluetooth':
  388. requestBluetoothPermission(options)
  389. break
  390. default:
  391. failRequestPermission(options, new PermissionFailImpl(9020003))
  392. }
  393. }
  394. export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) {
  395. const permissions = options.permissions
  396. if (permissions == null) {
  397. const emptyResult : RequestPermissionsResult = {
  398. results: [],
  399. allGranted: true,
  400. }
  401. succeedRequestPermissions(options, emptyResult)
  402. return
  403. }
  404. const permissionList = permissions!
  405. if (permissionList.length == 0) {
  406. const emptyResult : RequestPermissionsResult = {
  407. results: [],
  408. allGranted: true,
  409. }
  410. succeedRequestPermissions(options, emptyResult)
  411. return
  412. }
  413. let currentIndex = 0
  414. const collectedResults : PermissionGrantResult[] = []
  415. function requestNext() {
  416. if (currentIndex >= permissionList.length) {
  417. let allGranted = true
  418. for (let i = 0; i < collectedResults.length; i++) {
  419. if (!collectedResults[i].granted) {
  420. allGranted = false
  421. break
  422. }
  423. }
  424. const finalResult : RequestPermissionsResult = {
  425. results: collectedResults,
  426. allGranted: allGranted,
  427. }
  428. succeedRequestPermissions(options, finalResult)
  429. return
  430. }
  431. const permission = permissionList[currentIndex]
  432. currentIndex += 1
  433. requestPermission({
  434. permission: permission,
  435. success: (res : PermissionGrantResult) => {
  436. collectedResults.push(res)
  437. requestNext()
  438. },
  439. fail: (err : PermissionFailImpl) => {
  440. collectedResults.push(createRequestResult(permission, 'denied'))
  441. failRequestPermissions(options, err)
  442. },
  443. })
  444. }
  445. requestNext()
  446. }
  447. export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) {
  448. const settingsUrl = URL(string = UIApplication.openSettingsURLString)
  449. if (settingsUrl == null) {
  450. failOpenAppSettings(options, new PermissionFailImpl(9020004))
  451. return
  452. }
  453. DispatchQueue.main.async(execute = (): void => {
  454. UIApplication.shared.open(settingsUrl!)
  455. const resultPermission = options.permission != null ? options.permission! : 'camera'
  456. succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted'))
  457. })
  458. }
  459. export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult {
  460. return {
  461. permissions: SUPPORTED_PERMISSIONS,
  462. }
  463. }
  464. function mapIOSBluetoothState(state : CBManagerState) : string {
  465. switch (state) {
  466. case CBManagerState.poweredOn:
  467. return '已开启'
  468. case CBManagerState.poweredOff:
  469. return '已关闭'
  470. case CBManagerState.unsupported:
  471. return '不支持'
  472. case CBManagerState.unauthorized:
  473. return '未授权'
  474. case CBManagerState.resetting:
  475. return '重置中'
  476. case CBManagerState.unknown:
  477. return '未知'
  478. default:
  479. return '未知'
  480. }
  481. }
  482. export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo {
  483. const manager = new CBCentralManager()
  484. const state = manager.state
  485. return {
  486. available: state != CBManagerState.unsupported,
  487. enabled: state == CBManagerState.poweredOn,
  488. stateText: mapIOSBluetoothState(state),
  489. stateCode: state as number,
  490. }
  491. }