一、版本权限变更
1.1 前台Service
Android 10 | 获取定位需要Manifest配置 |
Android 11 | 增加了摄像头和麦克风需要Manifest配置 |
PermissionX | 正常代码申请,记得在Manifest中对Service配置就行。 |
<service ...
android:foregroundServiceType="location" //10
android:foregroundServiceType="location|camera|microphone" //11
/>
1.2 后台定位
后台定位:ACCESS_BACKGROUND_LOCATION 前台定位:ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION | |
Android 10 | 后台定位和前台定位需要一起申请(不具备前台资格怎么可能具备后台) 【始终允许】表示都同意 【仅在使用时允许】表示只允许前台 【拒绝】表示都不允许 |
Android 11 | 同时申请后台和前台会崩溃,申请后台之前需要已经授权了前台。 |
PermissionX | 同时申请前台和后台权限会就行,会自动处理:≤9只申请前台,10同时申请,11分开申请自动最后申请后台。 |
1.3 蓝牙
Android 12 | 修复使用蓝牙扫描附近设备的时候需要申请定位权限(蓝牙不是运行时权限,定位是),被拆封成三个运行时权限,用到哪个申请哪个(属于同一个权限组): 【BLUETOOTH_SCAN】用于使用蓝牙扫描附件其他的蓝牙设备 【BLUETOOTH_ADVERTISE】用于允许当前的设备被其他的蓝牙设备所发现 【BLUETOOTH_CONNECT】用于连接之前已经配对过的蓝牙设备 |
//≥12用到哪个申请哪个(都是同一个权限组)
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN)
add(Manifest.permission.BLUETOOTH_ADVERTISE)
add(Manifest.permission.BLUETOOTH_CONNECT)
}
1.4 应用安装
Android 8 | 8之前只要用户在手机上开启“允许安装未知来源的应用”选项就可以任意安装APP,8之后每次安装都会自动让用户同意一遍。对于开发者来说只要Manifest申请权限就行,但国内魔改ROM会在第一次安装时提醒,未授权后面不会再提醒。手动代码申请一下就行。 |
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
add(Manifest.permission.REQUEST_INSTALL_PACKAGES)
1.5 媒体文件
Android 13 | 当 targetSsk≥33 时申请 READ_EXTERNAL_STORAGE 无效果,取而代之的是更加细分的权限,图片音频视频需要申请以下三种权限,其他文件需要跳转到系统文件管理器,详见MediaStore。 【READ_MEDIA_IMAGES】图片 【READ_MEDIA_VIDEO】视频 【READ_MEDIA_AUDIO】音频 |
//≥13用到哪个申请哪个(图片视频是同一个权限组,音频是别的)
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.READ_MEDIA_IMAGES)
add(Manifest.permission.READ_MEDIA_AUDIO)
add(Manifest.permission.READ_MEDIA_VIDEO)
}
1.6 通知
Android 8 | 加入通知渠道,桌面图标角标。 |
Android 13 | 13之前系统默认APP有通知权限可以随时发,13之后发的时候需要申请,也不能像之前那样检测被关闭后跳转引导开启。 |
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.POST_NOTIFICATIONS)
}
//或者使用这个就不用手动判断了
add(PermissionX.permission.POST_NOTIFICATIONS)
1.7 后台运动传感器权限
Android 13 | 13之前申请了前后台都可以使用,13之后只能前台用,多了一个后台,并且需要前台通过了才能申请后台,一起申请还会同时被拒。 |
PermissionX | 两个一起申请,并对后台做SDK判断 |
<uses-permission android:name="android.permission.BODY_SENSORS"/>
<uses-permission android:name="android.permission.BODY_SENSORS_BACKGROUND"/>
add(Manifest.permission.BODY_SENSORS)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.BODY_SENSORS_BACKGROUND)
}
1.8 附近WiFi设备权限
Android 13 | 修复使用Wifi调用热点、直连、RTTT等功能的时候需要申请定位权限(Wifi不是运行时权限,定位是),被拆封成四个运行时权限,用到哪个申请哪个(属于同一个权限组): 【BLUETOOTH_SCAN】 【NEARBY_WIFI_DEVICES】 |
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"/>
add(Manifest.permission.NEARBY_WIFI_DEVICES)
二、使用 PermissionX
默认弹窗上的文字已适配国际化(只需要保证自己传入的文字使用@String)、颜色已适配深色模式。
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
2.1 配置Manifest
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CAMERA" />
2.2 权限申请 & 回调处理
//定义申请的权限清单
val requestList = ArrayList<String>().apply {
//一般添加权限
add(Manifest.permission.CALL_PHONE)
//有些权限存在版本区别,需要判断
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
add(Manifest.permission.BLUETOOTH_SCAN) //12的蓝牙
}
}
if (requestList.isNotEmpty()) {
//初始化
PermissionX.init(this)
//要申请的权限列表(可直接传入上方的requestList)
.permissions(Manifest.permission.CALL_PHONE, Manifest.permission.CAMERA)
//(可选)修改默认对话框的颜色(浅色主题、深色主题)(默认已适配深色模式)
.setDialogTintColor(Color.parseColor("#008577"), Color.parseColor("#83e8dd"))
//(可选)在申请前先弹窗说明(下方通过beforeRequest区分)
.explainReasonBeforeRequest()
//弹窗申请原因(提供功能函数的上下文对象,被拒绝或者还未申请的权限清单,是否是申请前)
.onExplainRequestReason { scope, deniedList, beforeRequest ->
if (beforeRequest) {
//申请前说明(上方需要添加.explainReasonBeforeRequest())
scope.showRequestReasonDialog(
deniedList,"为了保证程序正常工作,请您同意以下权限申请","我已明白")
} else {
//拒绝后说明(会再次申请)
val filteredList = deniedList.filter {
it == Manifest.permission.CAMERA
}
//使用默认的弹窗
scope.showRequestReasonDialog(
filteredList,"摄像机权限是程序必须依赖的权限","同意","拒绝")
//(可选)使用自定义弹窗
// val str = "摄像机权限是程序必须依赖的权限"
// val myDialog = MyDialog(context, str, deniedList)
// scope.showRequestReasonDialog(myDialog)
}
}
//针对被拒绝且不再询问的权限进行解释并跳转到系统设置
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
}
//申请权限并接收结果(是否全通过、通过的权限名单、拒绝的权限名单)
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}
}
2.3 创建自定义弹窗 (可选)
2.3.1 通过 Dialog
//自定义弹窗
class MyDialog(context: Context, val str: String, val permissions: List<String>) : RationaleDialog(context) {
private val groupSet = HashSet<String>()
private lateinit var negativeBtn: Button
private lateinit var positiveBtn: Button
private lateinit var permissionsLayout: LinearLayout
//一个权限组包含多个权限
private val permissionMap = mapOf(
Manifest.permission.READ_CALENDAR to Manifest.permission_group.CALENDAR,
Manifest.permission.WRITE_CALENDAR to Manifest.permission_group.CALENDAR,
Manifest.permission.READ_CALL_LOG to Manifest.permission_group.CALL_LOG,
Manifest.permission.WRITE_CALL_LOG to Manifest.permission_group.CALL_LOG,
Manifest.permission.PROCESS_OUTGOING_CALLS to Manifest.permission_group.CALL_LOG,
Manifest.permission.CAMERA to Manifest.permission_group.CAMERA,
Manifest.permission.READ_CONTACTS to Manifest.permission_group.CONTACTS,
Manifest.permission.WRITE_CONTACTS to Manifest.permission_group.CONTACTS,
Manifest.permission.GET_ACCOUNTS to Manifest.permission_group.CONTACTS,
Manifest.permission.ACCESS_FINE_LOCATION to Manifest.permission_group.LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION to Manifest.permission_group.LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION to Manifest.permission_group.LOCATION,
Manifest.permission.RECORD_AUDIO to Manifest.permission_group.MICROPHONE,
Manifest.permission.READ_PHONE_STATE to Manifest.permission_group.PHONE,
Manifest.permission.READ_PHONE_NUMBERS to Manifest.permission_group.PHONE,
Manifest.permission.CALL_PHONE to Manifest.permission_group.PHONE,
Manifest.permission.ANSWER_PHONE_CALLS to Manifest.permission_group.PHONE,
Manifest.permission.ADD_VOICEMAIL to Manifest.permission_group.PHONE,
Manifest.permission.USE_SIP to Manifest.permission_group.PHONE,
Manifest.permission.ACCEPT_HANDOVER to Manifest.permission_group.PHONE,
Manifest.permission.BODY_SENSORS to Manifest.permission_group.SENSORS,
Manifest.permission.ACTIVITY_RECOGNITION to Manifest.permission_group.ACTIVITY_RECOGNITION,
Manifest.permission.SEND_SMS to Manifest.permission_group.SMS,
Manifest.permission.RECEIVE_SMS to Manifest.permission_group.SMS,
Manifest.permission.READ_SMS to Manifest.permission_group.SMS,
Manifest.permission.RECEIVE_WAP_PUSH to Manifest.permission_group.SMS,
Manifest.permission.RECEIVE_MMS to Manifest.permission_group.SMS,
Manifest.permission.READ_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE to Manifest.permission_group.STORAGE,
Manifest.permission.ACCESS_MEDIA_LOCATION to Manifest.permission_group.STORAGE
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_permission)
val messageText: TextView = findViewById(R.id.messageText)
negativeBtn = findViewById(R.id.negativeBtn)
positiveBtn = findViewById(R.id.positiveBtn)
permissionsLayout = findViewById(R.id.permissionsLayout)
messageText.text = str
buildPermissionsLayout()
window?.let {
val param = it.attributes
val width = (context.resources.displayMetrics.widthPixels * 0.8).toInt()
val height = param.height
it.setLayout(width, height)
}
}
//确定按钮
override fun getPositiveButton(): View {
return positiveBtn
}
//取消按钮(弹窗不可取消就返回null)
override fun getNegativeButton(): View {
return negativeBtn
}
//要申请的权限列表
override fun getPermissionsToRequest(): List<String> {
return permissions
}
//申请是需要使用权限名,一个权限通过后该组其他权限会一起通过,但只需要显示组名给用户看
private fun buildPermissionsLayout() {
for (permission in permissions) {
val permissionGroup = permissionMap[permission]
if (permissionGroup != null && !groupSet.contains(permissionGroup)) {
val textView = LayoutInflater.from(context).inflate(R.layout.textview_permission, permissionsLayout, false) as TextView
textView.text =context.packageManager.getPermissionGroupInfo(permissionGroup, 0).loadLabel(context.packageManager)
permissionsLayout.addView(textView)
groupSet.add(permissionGroup)
}
}
}
}
2.3.2 通过 DialogFragment
//具体实现和上面的 Dialog 方式一样
class MyDialogFragment(context: Context, val str: String, val permissions: List<String>) : RationaleDialogFragment() {
override fun getPositiveButton(): View { }
override fun getNegativeButton(): View? { }
override fun getPermissionsToRequest(): MutableList<String> { }
}
2.4 特殊权限申请
特殊权限,既不属于普通权限也不属于危险权限,申请的时候需要使用intent跳转到系统界面让用户开启。
悬浮窗 | Android 6开始,只在自己的APP内是不需要申请权限的,悬浮在其他APP上需要。 | 【Manifest权限名称】<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> 【intent跳转的action】Settings.ACTION_MANAGE_OVERLAY_PERMISSION 【判断是否授权的API】Settings.canDrawOverlays(context) |
修改系统设置 | Android 6开始,允许APP读写系统设置。 | 【Manifest权限名称】<uses-permission android:name="android.permission.WRITE_SETTINGS" /> 【intent跳转的action】Settings.ACTION_MANAGE_WRITE_SETTINGS 【判断是否授权的API】Settings.System.canWrite(context) |
管理外置存储 | Android 11开始,强制启用作用域存储 Scoped Storage 使得所有APP不再对SD卡进行全局读写(10就有了),允许读整个SD卡进行读写。 | 【Manifest权限名称】<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> 【intent跳转的action】Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 【判断是否授权的API】Environment.isExternalStorageManager() |
//手写
if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(context)) {
showFloatView()
} else {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
} else {
showFloatView()
}
//使用PermissionX
PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
.onExplainRequestReason { scope, deniedList ->
val message = "PermissionX需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList",Toast.LENGTH_SHORT).show()
}
}
三、纯手写示例
<uses-permission android:name="android.permission.CALL_PHONE" />
//定义请求码(唯一值),不能为负数
val CALL_PERMISSION = 1
//申请权限后重写这个回调(对用户选择结果进行处理)
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
//根据对应的请求码去分类处理
when (requestCode) {
CALL_PERMISSION -> {
//申请通过就执行方法
if (grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRANTED) {
call()
} else {
//申请被拒绝就用对话框、吐司等方式提示
Toast.makeText(this,"不授权怎么用?",Toast.LENGTH_SHORT).show()
}
}
}
}
//对call()进行封装
fun callWrapper(){
//拿到权限状态
val permission = ContextCompat.checkSelfPermission(this, android.Manifest.permission.CALL_PHONE)
//没有授权就申请,有授权就直接调用
if(permission != PackageManager.PERMISSION_GRANTED){
//参数一当前Activity,参数二把要申请的权限名放入数组中,参数三是唯一的请求码
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CALL_PHONE),CALL_PERMISSION)
} else {
call()
}
}
//具体涉及涉及风险权限的业务代码
private fun call(){
try {
val intent= Intent(Intent.ACTION_CALL)
intent.data= Uri.parse("tel:10086")
startActivity(intent)
}catch (e:SecurityException){
e.printStackTrace()
}
}