从android 6.0(API 级别 23)开始,android引入了运行时权限,用户开始在应用运行时向其授予权限,而不是在应用安装时向其授予权限,如果应用的某项功能需要使用到受运行时权限保护的资源(例如相机、位置、麦克风等),但在运行该功能前没有动态地申请相应的权限,那么在调用该功能时就会抛出SecurityException异常, android 6.0已经推出了很多年了,相信大家对于运行时权限的申请过程已经非常的熟悉,但是android的运行时权限的申请过程一直都是非常的繁琐的,主要有两步:
1、在需要申请权限的地方检查该权限是否被同意,如果同意了就直接执行,如果不同意就动态申请权限;
2、重写Activity或Fragment的onRequestPermissionsResult方法,在里面根据grantResults数组判断权限是否被同意,如果同意就直接执行,如果不同意就要进行相应的提示,如果用户勾选了“don’t ask again”,还要引导用户去“Settings”界面打开权限,这时还要重写onActivityResult判断权限是否被同意。
就是这简单的两步,却夹杂了大量的if else语句,不但不优雅,而且每次都要写重复的样板代码,可能android的开发者也意识到了这一点,在最新androidx中引入了activity result api,通过activity result api你可以不需要自己管理requestCode,只需要提供需要请求的权限和处理结果的回调就行,让权限请求简单了一点,但是如果在权限请求的过程中,用户点击拒绝或者拒绝并不再询问,那么我们还是需要自己处理这些情况。
权限分类
android中所有的预定义权限(不包括厂商自定义的)都可以在Manifest.permission这个静态类中找到定义,android把权限分为四类:普通权限、签名权限、危险权限和特殊权限,每一种类型的权限都分配一个对应的Protection Level,分别为:normal、signature、dangerous和appop,下面简单介绍一下这四种类型的权限:
- 普通权限
普通权限也叫正常权限,Protection Level为normal,它不需要动态申请,你只需要在AndroidManifest.xml中静态地声明,然后系统在应用安装时就会自动的授予该应用相应的权限,当应用获得授权时,它就可以访问应用沙盒外受该普通权限保护地数据或操作,这些数据或操作不会泄漏或篡改用户的隐私,对用户或其他应用几乎没有风险。 - 签名权限
这类权限我们用得比较少,它只对拥有相同签名的应用开放,Protection Level为signature,它也不需要动态申请,例如应用A在AndroidManifest.xml中自定义了一个permission且在权限标签中加入android:protectionLevel=”signature”,表示应用A声明了一个签名权限,那么应用B想要访问应用A受该权限保护的数据时,必须要在AndroidManifest.xml中声明该权限,同时要用与应用A相同的签名打包,这样系统在应用B安装时才会自动地授予应用B该权限,应用B在获得授权后就可以访问该权限控制的数据,其他应用即使知道这个权限,也在AndroidManifest.xml中声明了该权限,但由于应用签名不同,安装时系统不会授予它该权限,这样其他应用就无法访问受该权限保护的数据。
还有一些签名权限不会供第三方应用程序使用,只会供系统预装应用使用,这种签名权限的Protection Level为signature和privileged。 - 危险权限
危险权限也叫运行时权限,Protection Level为dangerous,跟普通权限相反,一旦应用获取了该类权限,用户的隐私数据就会面临被泄露或篡改的风险,所以如果你想使用该权限保护的数据或操作,就必须在AndroidManifest.xml中静态地声明需要用到的危险权限,并在访问这些数据或操作前动态的申请权限,系统就会弹出一个权限请求弹窗征求用户的同意,除非用户同意该权限,否则你不能使用该权限保护的数据或操作。
所有的危险权限都有对应的权限组,android预定义了11个权限组(根据android 11总结),这11个权限组中包含了30个危险权限和几个普通权限,当我们动态的申请某个危险权限时,都是按权限组申请的,当用户一旦同意授权该危险权限,那么该权限所对应的权限组中的其他在AndroidManifest.xml中注册的权限也会同时被授权,android预定义的11个权限组包含的危险权限如下:
Permission | GroupDangerous Permissions |
---|---|
CALENDAR (日历) | READ_CALENDAR 、WRITE_CALENDAR |
CALL_LOG (通话记录,Added in android 29) | READ_CALL_LOG 、 WRITE_CALL_LOG 、PROCESS_OUTGOING_CALL |
SCAMERA (相机) | CAMERA |
CONTACTS (通讯录) | READ_CONTACTS 、WRITE_CONTACTS 、GET_ACCOUNTS |
LOCATION (位置信息) | ACCESS_COARSE_LOCATION 、ACCESS_FINE_LOCATION 、ACCESS_BACKGROUND_LOCATION (Added in android 10) |
MICROPHONE (麦克风) | RECORD_AUDIO |
PHONE (电话) | READ_PHONE_NUMBERS READ_PHONE_STATE CALL_PHONE ANSWER_PHONE_CALLS ADD_VOICEMAIL USE_SIP ACCEPT_HANDOVER (Added in android 9) |
SENSORS (身体传感器) | BODY_SENSORS |
SMS (短信) | READ_SMS RECEIVE_WAP_PUSH RECEIVE_SMS RECEIVE_MMS SEND_SMS |
STORAGE (存储空间) | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE ACCESS_MEDIA_LOCATION (Added in android 10) |
ACTIVITY_RECOGNITION (身体活动,Added in android 10) | ACTIVITY_RECOGNITION (Added in android 10) |
- 特殊权限
特殊权限用于保护一些特定的应用程序操作,Protection Level为appop,使用前也需要在AndroidManifest.xml中静态地声明,也需要动态的申请,但是它不同于危险权限的申请,危险权限的申请会弹出一个对话框询问你是否同意,而特殊权限的申请需要跳转到指定的设置界面,让你手动点击toggle按钮确认是否同意,截止到android 11,我了解到的常用的5个特殊权限为:
- SYSTEM_ALERT_WINDOW:允许应用在其他应用的顶部绘制悬浮窗,当你创建的悬浮窗是TYPE_APPLICATION_OVERLAY类型时需要申请这个权限;
- WRITE_SETTINGS:允许应用修改系统设置,当你需要修改系统参数Settings.System时需要申请该权限,例如修改系统屏幕亮度等;
- REQUEST_INSTALL_PACKAGES: 允许应用安装未知来源应用,android 8.0以后当你在应用中安装第三方应用时需要申请这个权限,否则不会跳转到安装界面;
- PACKAGE_USAGE_STATS:允许应用收集其他应用的使用信息,当你使用UsageStatsManager相关Api获取其他应用的信息时需要申请这个权限;
- MANAGE_EXTERNAL_STORAGE(Added in android 11):允许应用访问作用域存储(scoped storage)中的外部存储,android 11以后强制新安装的应用使用作用域存储,但是对于文件管理器这一类的应用它们需要管理整个SD卡上的文件,所以针对这些特殊应用可以申请这个权限来获得对整个SD卡的读写权限,当应用授予这个权限后,它就可以访问文件的真实路径,注意这个权限是很危险的,声明这个权限上架应用时可能需要进行审核.
除了特殊权限,LOCATION权限组中的位置权限也有点特殊,需要注意一下,位置信息的获取不仅依赖位置权限的动态申请还依赖系统定位开关,如果你没有打开定位开关就申请了位置权限,那么就算用户同意授权位置权限,应用通过Location相关Api也无法获取到位置信息,所以申请位置权限前,最好先通过LocationManager#isProviderEnabled方法判断是否打开定位开关后再进行位置权限的申请,如果没有打开定位开关需要先跳转到设置界面打开定位开关,伪代码如下:val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) or locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { //请求位置权限 } else { //跳转到开启定位的地方 Toast.makeText(this, "检测到未开启定位服务,请开启", Toast.LENGTH_SHORT).show() val intent = Intent().apply { action = Settings.ACTION_LOCATION_SOURCE_SETTINGS } startActivityForResult(intent, REQUEST_CODE_LOCATION_PROVIDER) }