引言

我们在应用开发中如果需要联网就需要在AndroidManifest.xml文件中添加网络访问权限。在我们新安装应用时,往往会弹出提示框问我们是否允许系统获取定位、读取联系人、读取短信等权限。那这些权限有什么用?区别是什么?为什么会有运行时权限这一说法……下面让小编来一一梳理并解答。

运行时权限

Android的权限机制并不是什么新鲜事物,从系统的第一个版本开始就已经存在了。但其实之前Android的权限机制在保护安全和隐私等方面起到的作用十分有限,尤其是一些大家离不开的常用软件,非常容易“店大欺客”。为此,Android开发团队在Android6.0系统中引入了运行时权限这个功能,从而更好地保护了用户的安全和隐私。

Android权限机制详解

如下,监听开机广播需要开启广播接收的权限:

image-20220913165504245

因为监听开机广播涉及了用户设备的安全,因此必须在AndroidManifest.xml文件中加入权限声明,否则我们的程序就会崩溃。

那么问题来了,加入了这句权限声明后,对于用户来说到底有什么影响呢?为什么这样就可以保护设备的安全了呢?

其实用户主要在两方面得到保护。一方面,如果用户在低于Android6.0系统的设备商安装该程序,会在安装界面弹出一连串提示。这样用户就可以知晓该程序一共申请了哪些权限,从而决定是否安装这个程序。另一方面,用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况。这样该程序申请的权限就尽收眼底,什么都瞒不过用户,以此保证应用程序不会出现滥用权限的情况。

但是,很多时候我们离不开的常用软件普遍存在滥用权限的情况,不管到底有没有用得到,反正先申请了再说。

考虑到上述问题,Android6.0以后加入了动态申请权限功能。但是,如果安装一个应用需要不停地授权很多权限,就很繁琐,用户体验极差。那么就将权限进行了划分——普通权限与危险权限。普通权限会默认开启,而危险权限需要用户进行授权。

危险权限

下面列出Android危险权限,除了这些剩余的都是普通权限。

权限组名 权限名
CALENDAR(日历) READ_CALENDAR、WRITE_CALENDAR
CAMERA(相机) CAMERA、READ_CONTACTS
CONTACTS(联系人) WRITE_CONTACTS、GET_ACCOUNTS
LOCATION(位置) ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION
MICROPHONE(麦克风) RECORD_AUDIO、 READ_PHONE_STATE、CALL_PHONE、ERAD_CALL_LOG
PHONE(手机) WRITE_CALL_LOG、ADD_VOICEMAIL、USE_SIP、PROCESS_OUTGOING_CALLS
SENSORS(传感器) BODY_SENSORS、 SEND_SMS、RECEIVE_SMS
SMS(短信) READ_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS
STORAGE(存储卡) READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE

这张表看起来不怎么轻松,因为里面的权限不都是能接触到的。不过没有关系,你不需要了解表格中每个权限的作用,只需要把它当成一个参照表来看就行了。每当需要使用一个权限时,可以查表,如果该权限出现在这张表中,那么就需要动态申请权限。

另外注意,表格中每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其它权限都会被系统自动开启。

特别注意:不要基于此规则来实现任何逻辑,因为Android系统随时有可能调整权限的分组。

在程序运行时申请权限

运行效果

录制_2022_09_13_17_51_50_995

class MainActivity : AppCompatActivity() {
companion object{
const val REQUEST_CODE = 1
}
lateinit var binding:ActivityMainBinding;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.Call.setOnClickListener {
if (ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CALL_PHONE)
, REQUEST_CODE)
}else{
call()
}
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when(requestCode){
REQUEST_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call()
}else{
Toast.makeText(this,"You denied the permission",Toast.LENGTH_LONG).show()
}
}
}
}

private fun call(){
try {
val intent = Intent(Intent.ACTION_CALL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}catch (e:SecurityException){
e.printStackTrace()
}
}
}

上面的代码覆盖了运行时权限的完整流程,下面我们来具体分析一下。

  • 第一步就是要先判断用户是不是已经给我们进行了授权,借助的是ContextCompat.checkSelfPermission()方法接收两个参数:

    • Context
    • 具体的权限名,比如打电话的权限名Manifest.permission.CALL_PHONE

    然后我们使用方法的返回值与PackageManager.PERMISSION_GRANTED做比较,相等就说明用户已经授权,不等就表示用户没有权限。

  • 如果已经授权了,直接执行拨打电话的逻辑就可以了,这里我们把拨打电话的逻辑封装到call()方法中。如果没有授权的话,则需要调用ActivityCompat.requestPermissions()方法向用户申请权限。requestPermissions()接收3个参数:

    • 第一个参数要求是Activity实例
    • 第二个参数是String数组,我们把要申请的权限名放到数组中即可
    • 第三个要求是请求码,只要是唯一值就可以
  • 调用完requestPermissions()方法之后,系统会弹出一个权限申请的对话框,用户可以选择同意我们的权限申请。无论是哪种结果,最终都会回调 onRequestPermissionsResult()方法中,而授权的结果则会封装在grantResults参数中。这里我们只需要判断一下最后的授权结果,如果用户同意的话,就调用call()方法拨打电话;如果用户拒绝的话,我们只能放弃操作,并且弹出一条失败的提示。