import Build from 'android.os.Build' import Intent from 'android.content.Intent' import Settings from 'android.provider.Settings' import Uri from 'android.net.Uri' import ContextCompat from 'androidx.core.content.ContextCompat' import ActivityCompat from 'androidx.core.app.ActivityCompat' import PackageManager from 'android.content.pm.PackageManager' import BluetoothAdapter from 'android.bluetooth.BluetoothAdapter' import { UTSAndroid } from 'io.dcloud.uts' import { BluetoothDemoInfo, CheckPermission, CheckPermissionOptions, CheckPermissions, CheckPermissionsOptions, CheckPermissionsResult, CheckPermissionsSync, CheckPermissionSync, GetSupportedPermissionsResult, GetSupportedPermissionsSync, GetBluetoothDemoInfoSync, OpenAppSettings, OpenAppSettingsOptions, PermissionCheckResult, PermissionFail, PermissionKey, PermissionGrantResult, PermissionStatus, RequestPermission, RequestPermissionOptions, RequestPermissions, RequestPermissionsOptions, RequestPermissionsResult, } from '../interface.uts' import { PermissionFailImpl } from '../unierror' const ANDROID_API_TIRAMISU : Int = 33 const ANDROID_API_S : Int = 31 function getAppTargetSdkVersion() : Int { const activity = UTSAndroid.getUniActivity() if (activity == null) { return 0 } return activity.getApplicationInfo().targetSdkVersion } /** 设备 API 与宿主 targetSdkVersion 均满足时才使用 READ_MEDIA_*(UTS 运行时强校验 targetSdk) */ function shouldUseReadMediaPermissions() : boolean { if (Build.VERSION.SDK_INT < ANDROID_API_TIRAMISU) { return false } return getAppTargetSdkVersion() >= ANDROID_API_TIRAMISU } /** 设备 API 与宿主 targetSdkVersion 均满足时才使用 BLUETOOTH_CONNECT / BLUETOOTH_SCAN */ function shouldUseBluetoothRuntimePermissions() : boolean { if (Build.VERSION.SDK_INT < ANDROID_API_S) { return false } return getAppTargetSdkVersion() >= ANDROID_API_S } const SUPPORTED_PERMISSIONS : PermissionKey[] = [ 'camera', 'photoLibrary', 'storage', 'readStorage', 'location', 'coarseLocation', 'microphone', 'contacts', 'writeContacts', 'bluetooth', 'phone', 'phoneState', 'sms', 'readSms', 'receiveSms', 'overlay', 'writeSettings', ] 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 resolveAndroidPermissions(permission : PermissionKey) : string[] | null { switch (permission) { case 'camera': return ['android.permission.CAMERA'] case 'photoLibrary': if (shouldUseReadMediaPermissions()) { return ['android.permission.READ_MEDIA_IMAGES'] } return ['android.permission.READ_EXTERNAL_STORAGE'] case 'storage': if (shouldUseReadMediaPermissions()) { return ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO'] } return ['android.permission.WRITE_EXTERNAL_STORAGE', 'android.permission.READ_EXTERNAL_STORAGE'] case 'readStorage': if (shouldUseReadMediaPermissions()) { return ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO'] } return ['android.permission.READ_EXTERNAL_STORAGE'] case 'location': return ['android.permission.ACCESS_FINE_LOCATION'] case 'coarseLocation': return ['android.permission.ACCESS_COARSE_LOCATION'] case 'microphone': return ['android.permission.RECORD_AUDIO'] case 'contacts': return ['android.permission.READ_CONTACTS'] case 'writeContacts': return ['android.permission.WRITE_CONTACTS'] case 'bluetooth': if (shouldUseBluetoothRuntimePermissions()) { return ['android.permission.BLUETOOTH_CONNECT', 'android.permission.BLUETOOTH_SCAN'] } return ['android.permission.BLUETOOTH', 'android.permission.BLUETOOTH_ADMIN'] case 'phone': return ['android.permission.CALL_PHONE'] case 'phoneState': return ['android.permission.READ_PHONE_STATE'] case 'sms': return ['android.permission.SEND_SMS'] case 'readSms': return ['android.permission.READ_SMS'] case 'receiveSms': return ['android.permission.RECEIVE_SMS'] case 'overlay': case 'writeSettings': return [] default: return null } } function isPermissionGranted(permission : PermissionKey) : boolean { const activity = UTSAndroid.getUniActivity() if (activity == null) { return false } if (permission == 'overlay') { return Settings.canDrawOverlays(activity) } if (permission == 'writeSettings') { return Settings.System.canWrite(activity) } const androidPermissions = resolveAndroidPermissions(permission) if (androidPermissions == null) { return false } if (androidPermissions.length == 0) { return false } return UTSAndroid.checkSystemPermissionGranted(activity, androidPermissions) } function resolvePermissionStatus(permission : PermissionKey) : PermissionStatus { const androidPermissions = resolveAndroidPermissions(permission) if (androidPermissions == null) { return 'unsupported' } if (permission == 'overlay' || permission == 'writeSettings') { return isPermissionGranted(permission) ? 'granted' : 'notDetermined' } const activity = UTSAndroid.getUniActivity() if (activity == null) { return 'denied' } let allGranted = true let hasDenied = false for (let i = 0; i < androidPermissions.length; i++) { const androidPermission = androidPermissions[i] const granted = ContextCompat.checkSelfPermission(activity, androidPermission) == PackageManager.PERMISSION_GRANTED if (granted) { continue } allGranted = false if (ActivityCompat.shouldShowRequestPermissionRationale(activity, androidPermission)) { hasDenied = true } } if (allGranted) { return 'granted' } if (hasDenied) { return 'denied' } return 'notDetermined' } 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 : PermissionFail) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function failRequestPermissions(options : RequestPermissionsOptions, err : PermissionFail) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function failOpenAppSettings(options : OpenAppSettingsOptions, err : PermissionFail) { const failCallback = options.fail if (failCallback != null) { failCallback(err) } const completeCallback = options.complete if (completeCallback != null) { completeCallback(err) } } function requestOverlayPermission(options : RequestPermissionOptions) { const activity = UTSAndroid.getUniActivity() if (activity == null) { failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用')) return } if (Settings.canDrawOverlays(activity)) { succeedRequestPermission(options, createRequestResult(options.permission, 'granted')) return } const intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) intent.setData(Uri.parse('package:' + activity.getPackageName())) activity.startActivity(intent) succeedRequestPermission(options, createRequestResult(options.permission, 'notDetermined')) } function requestWriteSettingsPermission(options : RequestPermissionOptions) { const activity = UTSAndroid.getUniActivity() if (activity == null) { failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用')) return } if (Settings.System.canWrite(activity)) { succeedRequestPermission(options, createRequestResult(options.permission, 'granted')) return } const intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) intent.setData(Uri.parse('package:' + activity.getPackageName())) activity.startActivity(intent) succeedRequestPermission(options, createRequestResult(options.permission, 'notDetermined')) } function requestRuntimePermission(options : RequestPermissionOptions) { const activity = UTSAndroid.getUniActivity() const androidPermissions = resolveAndroidPermissions(options.permission) if (activity == null) { failRequestPermission(options, new PermissionFailImpl(9020006, 'Activity 不可用')) return } if (androidPermissions == null) { failRequestPermission(options, new PermissionFailImpl(9020003)) return } if (androidPermissions.length == 0) { failRequestPermission(options, new PermissionFailImpl(9020003)) return } if (UTSAndroid.checkSystemPermissionGranted(activity, androidPermissions)) { succeedRequestPermission(options, createRequestResult(options.permission, 'granted')) return } try { UTSAndroid.requestSystemPermission( activity, androidPermissions, (allGranted : boolean, _ : string[]) => { if (allGranted) { succeedRequestPermission(options, createRequestResult(options.permission, 'granted')) return } succeedRequestPermission(options, createRequestResult(options.permission, 'denied')) }, (_ : boolean, _grantedList : string[]) => { succeedRequestPermission(options, createRequestResult(options.permission, 'denied')) }, ) } catch (ignored) { failRequestPermission(options, new PermissionFailImpl(9020006, '权限申请失败,请确认 manifest 已声明对应权限且 targetSdkVersion 满足要求')) } } export const checkPermissionSync : CheckPermissionSync = function (permission : PermissionKey) : PermissionCheckResult { const status = resolvePermissionStatus(permission) return createCheckResult(permission, status) } 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) { const result = checkPermissionSync(options.permission) succeedCheckPermission(options, result) } export const checkPermissions : CheckPermissions = function (options : CheckPermissionsOptions) { const result = checkPermissionsSync(options.permissions) succeedCheckPermissions(options, result) } export const requestPermission : RequestPermission = function (options : RequestPermissionOptions) { if (options.permission == 'overlay') { requestOverlayPermission(options) return } if (options.permission == 'writeSettings') { requestWriteSettingsPermission(options) return } requestRuntimePermission(options) } export const requestPermissions : RequestPermissions = function (options : RequestPermissionsOptions) { const permissions = options.permissions if (permissions.length == 0) { const emptyResult : RequestPermissionsResult = { results: [], allGranted: true, } succeedRequestPermissions(options, emptyResult) return } let currentIndex = 0 const collectedResults : PermissionGrantResult[] = [] function requestNext() { if (currentIndex >= permissions.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 = permissions[currentIndex] currentIndex += 1 requestPermission({ permission: permission, success: (res : PermissionGrantResult) => { collectedResults.push(res) requestNext() }, fail: (err : PermissionFail) => { collectedResults.push(createRequestResult(permission, 'denied')) failRequestPermissions(options, err) }, }) } requestNext() } export const openAppSettings : OpenAppSettings = function (options : OpenAppSettingsOptions) { const activity = UTSAndroid.getUniActivity() if (activity == null) { failOpenAppSettings(options, new PermissionFailImpl(9020004, 'Activity 不可用')) return } try { const permission = options.permission let intent : Intent if (permission == 'overlay') { intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) intent.setData(Uri.parse('package:' + activity.getPackageName())) } else if (permission == 'writeSettings') { intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) intent.setData(Uri.parse('package:' + activity.getPackageName())) } else { intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.setData(Uri.parse('package:' + activity.getPackageName())) } activity.startActivity(intent) const resultPermission = permission != null ? permission : 'camera' succeedOpenAppSettings(options, createCheckResult(resultPermission, 'granted')) } catch (ignored) { failOpenAppSettings(options, new PermissionFailImpl(9020004)) } } export const getSupportedPermissionsSync : GetSupportedPermissionsSync = function () : GetSupportedPermissionsResult { return { permissions: SUPPORTED_PERMISSIONS, } } function mapAndroidBluetoothState(state : Int) : string { switch (state) { case BluetoothAdapter.STATE_OFF: return '已关闭' case BluetoothAdapter.STATE_TURNING_ON: return '正在打开' case BluetoothAdapter.STATE_ON: return '已开启' case BluetoothAdapter.STATE_TURNING_OFF: return '正在关闭' default: return '未知' } } export const getBluetoothDemoInfoSync : GetBluetoothDemoInfoSync = function () : BluetoothDemoInfo { const adapter = BluetoothAdapter.getDefaultAdapter() if (adapter == null) { return { available: false, enabled: false, stateText: '设备不支持蓝牙', stateCode: -1, } } const state = adapter.getState() return { available: true, enabled: state == BluetoothAdapter.STATE_ON, stateText: mapAndroidBluetoothState(state), stateCode: state, } }