Android基础系列:权限机制

3,572 阅读7分钟

一 权限机制

基础系列:Android权限请求机制,逻辑上是比较简单的,但代码使用起来,是特别枯燥。而且随着版本变化,部分权限请求方式也会随之变化。

权限的作用是保护 Android 用户的隐私。每个Android都是处在访问受限的沙盒中运行。但凡应用需要使用自己的沙盒外的资源或信息,就必须请求相应权限,如拨号,访问内存等。

在API22及以下,Android的权限是处于安装时请求,即在安装apk时,便列出所需权限,同意即安装,不同意则退出安装。从API23(Android6.0)开始,Android开始使用运行时权限机制,即用到再请求。若没有获得权限就进行相关操作,会抛出SecurityException异常

1.1 权限类型划分

权限API参考页面

1.1.1 权限级别

权限分为几种保护级别,直接影响是否需要进行运行时权限申请。

权限分为:

  1. 普通权限,对用户隐私影响或风险很少的操作,如Internet权限。此类权限,在应用清单声明后,app会自动授予
  2. 危险权限,可能会对用户隐私影响的权限,如读取联系人或读写存储文件,此类权限需要进行申请。
  3. 签名权限,只有声明相同签名文件才会授予,一般不使用。
  4. 特殊权限,SYSTEM_ALERT_WINDOWWRITE_SETTINGS等,此类权限比较特殊,用户如果需要他们,必须在应用清单声明后,发送请求用户授权的 intent,跳到设置界面,由用户手动允许
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, 2);

具体权限属于哪种类型,可以参考权限API参考页面

1.1.2 权限组

任何权限都可以属于某个权限组,如果应用已在相同权限组中被授予另一危险权限,系统将立即授予该权限,不需再弹出申请框。如某应用之前已请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统会自动授予该权限

Android10以下可以通过以下方法取得权限所属权限组:

context.packageManager.getPermissionInfo(permission, 0)

Android10及以上,移除权限组查询,只能自己维护一份危险权限对应表(以下代码转自郭神):

/**
 * Based on this link https://developer.android.com/about/versions/10/privacy/changes#permission-groups-removed
 * Since Android Q, we can not get the permission group name by permission name anymore.
 * So we need to keep a track of relationship between permissions and permission groups on every Android release since Android Q.
 */
@TargetApi(Build.VERSION_CODES.Q)
fun getPermissionMapOnQ() = 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
)

/**
 * Thankfully Android R has no permission added or removed than Android Q.
 */
@TargetApi(Build.VERSION_CODES.R)
fun getPermissionMapOnR() = getPermissionMapOnQ()

...

val permissionGroup = when(currentVersion) {
                Build.VERSION_CODES.Q -> {
                    getPermissionMapOnQ()[permission]
                }
                Build.VERSION_CODES.R -> {
                    getPermissionMapOnR()[permission]
                }
                else -> {
                    val permissionInfo = context.packageManager.getPermissionInfo(permission, 0)
                    permissionInfo.group
                }
            }

1.2 权限请求流程

1.2.1 权限声明

应用所需权限,必须在应用清单AndroidManifest.xml中通过<uses-permission>声明所需权限,如读写手机存储,及使用互联网:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.snazzyapp">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <uses-permission android:name="android.permission.INTERNET" />

    <application ...>
        ...
    </application>
</manifest>

对于普通权限,如android.permission.INTERNET,在应用清单声明之后即可自动获得,而对于读写手机存储空间这种危险权限,则需要在运行时进行动态请求

1.2.2 权限状态检查

每次需要用到一些危险权限时,都应该进行权限的检查和申请。用户随时可以撤销之前授予的权限。

检查是否已授予特定权限,可将该权限传给 ActivityCompat.checkSelfPermission() 方法。成功会返回 PackageManager.PERMISSION_GRANTED ,失败则是 PackageManager.PERMISSION_DENIED

应用权限有个比较有趣的方法,它会提示开发者,是否需要展示一些弹框说明为何要对权限进行申请,从而提高产品的人性化体验:ActivityCompat.shouldShowRequestPermissionRationale()

当用户拒绝了权限的申请,且没有点击“不再询问”时,该方法才会返回true. 用于实际开发时,可以用来判断用户是否勾选了“不再询问”,从而跳到设置界面,由用户手动授予权限

1.2.3 权限请求

(1)传统权限请求:

传统权限请求相对比较繁琐:

ActivityCompat.requestPermissions(
    this,
    arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE),
    REQUEST_CODE
)

调用该方法请求权限,对于权限的结果,是需要重写方法获取的:

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {

    if (requestCode != REQUEST_CODE) { //请求码判断是否为发送的权限请求
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        return
    }

    for (i in permissions.indices) {
        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
            Log.e("小新", "取得权限:${permissions[i]}")
        }
    }
}

传统权限请求,是比较繁琐的,不能链式调用,还得分开进行判断,严重降低了代码可读性。

(2)新api请求registerForActivityResult

依然范特稀西刚好有一篇文章讲到了新api:再见!onActivityResult!你好,Activity Results API!

Google针对这种情况,jetpack提供了新的权限请求api来简化这种请求过程(目前还处于beta版本)->

首先声明依赖:

    dependencies {
        
        // Java 
        implementation "androidx.activity:activity:1.2.0-beta01"  
        implementation 'androidx.fragment:fragment:1.3.0-beta01'
            
        // Kotlin
        implementation "androidx.activity:activity-ktx:1.2.0-beta01"  
        implementation 'androidx.fragment:fragment-ktx:1.3.0-beta01'
    
    }

注:

  • 需要请求一项权限,使用 RequestPermission
  • 需要同时请求多项权限,使用 RequestMultiplePermissions

先在 Activity 或 Fragment 的初始化逻辑中,注册ActivityResultCallback,保留对 registerForActivityResult()(类型为 ActivityResultLauncher)的返回值的引用。然后在需要申请的地方调用保存的 ActivityResultLauncher 实例的 launch() 方法。

...
val requestPermissionLauncher =
    registerForActivityResult(RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // 通过
        } else {
           //拒绝
        }
    }

...

requestPermissionLauncher.launch(
                Manifest.permission.WRITE_EXTERNAL_STORAGE)

(3)跳到app设置界面

当用户拒绝并点击了“不再询问”,通过以上请求,会直接返回申请被拒绝,此时只能跳转到用户相关设置界面,由用户手动点击授权

val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", this.packageName, null)
intent.data = uri
startActivityForResult(intent, FORWARD_TO_SETTING_CODE)

二 自定义权限

2.1 使用场景

自定义权限一般用于暴露出去的组件,提高安全性。Android允许一个应用(客户端)调用另一个应用(服务端)的组件。那么作为服务端的应用就得暴露相应的组件,客户端应用才能访问。对于暴露出去的组件,权限是非必须的,如果暴露的组件没有权限的话,那么任何的其他应用都可以来调用该组件,相对不安全;如果该组件申请了权限,那么只有拥有该权限的应用才能调用该组件。

<activity android:name=".MainActivity"
    android:exported="true">
    ...

对于这种没有声明权限的,其他任何app即可开启该activity(显式启动或隐式启动)

//intent.setClassName("完整包名","完整类名")
intent.setClassName("com.cyq.clockout","com.cyq.clockout.MainActivity")

当声明了权限,若第三方app在没有权限的情况下开启activity,则会抛出权限失败异常

<activity android:name=".MainActivity"
    android:exported="true"
    android:permission="com.cyq.TEST">
    ...

2.2 创建自定义权限

创建自定义的权限,只需要在app的AndroidManifest.xml中,用 <permission> 元素声明即可

<permission
    android:name="com.cyq.TEST"
    android:description="@string/des"
    android:label="@string/label"
    android:protectionLevel="dangerous"
    android:permissionGroup="android.permission-group.LOCATION"/>
  • android:protectionLevel:声明权限的级别;

  • android:permissionGroup:权限所属的权限组,在大多数情况下,应将其设置为android自带的标准系统组(在 android.Manifest.permission_group 中列出),但也可以自行定义权限组。如例子中使用Android自带的位置权限组来做申请,当使用权限申请ActivityCompat.requestPermissions时,系统会提示与该权限组关联的弹框

  • 还需要为权限提供标签和说明。这些是用户在查看权限列表 (android:label) 或有关单个权限的详细信息 (android:description) 时能够看到的字符串资源。label是在查看应用权限设置时可以看到的,而描述是在点击了权限时,弹出的描述框可以看到的。

图:标签

图:描述

三 参考链接

  1. Request app permissions
  2. 再见!onActivityResult!你好,Activity Results API!

四 ❤️ 感谢

  1. 如果觉得这篇内容对你有所帮忙,点赞支持下(👍)
  2. 关于纠错和建议:欢迎直接在留言分享记录(🌹)

- END -