这篇文章为大家系统的梳理一下 Android 权限相关的知识,在日常开发中,我们都用过权限,但是对于权限的一些细节我们可能掌握的还不够全面,这篇文章会全面的为大家介绍权限相关的知识。当然,本篇文章依然是参考了 Google 的官方文档:应用权限。
本文目录
一、认识 Android 权限
(一)Android 系统为什么需要权限?
Android 系统设置权限的目的是保护 Android 用户的隐私。对于用户的敏感数据 Android 应用程序必须向用户申请授权后才能访问(如联系人和短信),另外还包括某些系统功能(如摄像头、麦克风)的权限。根据功能的不同,系统可能会自动授予权限或提示用户批准请求。Android 安全架构的一个核心设计要点是,在默认情况下,没有应用程序可以执行任何可能对其他应用程序、操作系统或用户造成不利影响的操作。这包括读取或写入用户的私人数据(如联系人或电子邮件)、读取或写入另一个应用程序的文件、执行网络访问等等。
(二)权限分类
权限分为几个保护级别。保护级别影响是否需要运行时权限请求:
- Normal permissions 正常权限
- Signature permissions 签名权限
- Dangerous permissions 危险权限
需要我们了解的是正常权限和危险权限。
1.正常权限
正常的权限覆盖了应用程序需要访问沙箱之外的数据或资源的区域,但这些区域对用户隐私或其他应用程序的操作几乎没有风险。例如,设置时区的权限是正常的权限。
如果应用程序在它的清单中声明它需要一个正常的权限,系统会在安装时自动授予该权限。系统不提示用户授予正常权限,用户也不能撤销这些权限。
- ACCESS_LOCATION_EXTRA_COMMANDS
- ACCESS_NETWORK_STATE
- ACCESS_NOTIFICATION_POLICY
- ACCESS_WIFI_STATE
- BLUETOOTH
- BLUETOOTH_ADMIN
- BROADCAST_STICKY
- CHANGE_NETWORK_STATE
- CHANGE_WIFI_MULTICAST_STATE
- CHANGE_WIFI_STATE
- DISABLE_KEYGUARD
- EXPAND_STATUS_BAR
- GET_PACKAGE_SIZE
- INSTALL_SHORTCUT
- INTERNET
- KILL_BACKGROUND_PROCESSES
- MODIFY_AUDIO_SETTINGS
- NFC
- READ_SYNC_SETTINGS
- READ_SYNC_STATS
- RECEIVE_BOOT_COMPLETED
- REORDER_TASKS
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
- REQUEST_INSTALL_PACKAGES
- SET_ALARM
- SET_TIME_ZONE
- SET_WALLPAPER
- SET_WALLPAPER_HINTS
- TRANSMIT_IR
- UNINSTALL_SHORTCUT
- USE_FINGERPRINT
- VIBRATE
- WAKE_LOCK
- WRITE_SYNC_SETTINGS
2.危险权限
危险权限包括应用程序需要涉及用户私人信息的数据或资源的区域,也包括可能影响用户存储的数据或其他应用程序的操作的区域。例如,读取用户的联系人是一种危险的权限。如果一个应用程序声明它需要一个危险的权限,用户必须显式地授予该应用程序权限。在用户批准该权限之前,应用程序不能提供依赖于该权限的功能。
要使用危险的权限,应用程序必须在运行时提示用户授予权限。
(三)如何声明一个权限?
应用程序必须通过在清单文件(AndroidManifest.xml
)中使用 <uses-permission>
标记来公布它需要的权限。例如声明网络访问权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
//声明网络访问权限
<uses-permission android:name="android.permission.INTERNET"/>
<application ...>
...
</application>
</manifest>
如果在应用程序的清单文件中列出的是正常的权限(即不会对用户隐私或设备操作造成太大风险的权限),系统会自动将这些权限授予给应用程序。
如果在应用程序的清单中列出的是危险的权限(即可能影响用户隐私或设备正常操作的权限),则必须经过用户的同意才能授权相应的权限。
(四)Android 不同版本对危险权限的处理方式
Android 请求用户授予危险权限的方式取决于用户设备上运行的 Android 版本,以及我们在应用中设置的 targetSdkVersion
。主要有两种处理方式:
- 运行时请求:Android 6.0 以及更高的版本
- 安装时请求:Android 5.1.1 以及更低的版本
- 运行时请求:
如果手机 Android 系统的版本是 6.0 (API级别23) 或者更高,而应用程序的 targetSdkVersion
是 23 或者更高,用户在安装时不会收到任何应用程序权限通知。应用程序必须要求用户在运行时授予危险的权限。当应用程序请求权限时,用户会看到一个系统对话框,告诉用户应用程序试图访问哪个权限组。对话框包含一个拒绝和允许按钮。
如果用户拒绝权限请求,那么下一次应用程序请求该权限时,对话框将包含一个复选框,选中该复选框后,用户不会再收到权限申请提示。
下面通过申请拍照权限为例:
可以看到,第一次弹框时选择拒绝,第二次弹框出现了一个“不再询问”的复选框,勾选以后,再次拒绝,则之后都不会再弹出权限申请的对话框。
假如用户选择了“允许”,也不能表示应用就会一直拥有该权限。用户还可以进入系统设置页面,将之前那授予的权限关闭掉,因此,我们在开发中必须在运行时去检查和申请相应的权限,以防止在运行时出现 SecurityException
的错误,导致应用奔溃。
- 安装时请求:
如果手机 Android 系统的版本是 5.1.1 (API级别22) 或者更低,而应用程序的 targetSdkVersion
是 22 或者更低,系统会自动要求用户在安装时为应用程序授予所有危险的权限。
如果用户单击 Accept
,应用程序请求的所有权限都将被授予。如果用户拒绝权限请求,系统将取消应用程序的安装。如果应用程序更新需要额外的权限,用户在更新应用程序之前会被提示接受这些新的权限。
(五)特殊的两个权限
有两个权限的行为不像正常权限和危险权限:SYSTEM_ALERT_WINDOW
和 WRITE_SETTINGS
这是两个特别敏感的权限,所以大多数应用程序不应该使用它们。如果应用程序需要这些权限之一,它必须在清单中声明该权限,并发送一个意图请求用户的授权。系统通过向用户显示详细的管理屏幕来响应这个意图。
以申请 SYSTEM_ALERT_WINDOW
为例:
step 1:首先在清单文件中声明权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
step 2:申请权限(6.0 及其以上版本)
//在 6.0 以前的系统版本,悬浮窗权限是默认开启的,直接使用即可。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(context)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(intent);
return;
}
}
(六)权限组
Android 系统对所有的危险权限进行了分组,称为 权限组
。
权限组 | 权限 |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION ACCESS_COARSE_LOCATION |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP PROCESS_OUTGOING_CALLS |
SENSORS | BODY_SENSORS |
SMS | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
如果手机 Android 系统的版本是 6.0 (API级别23) 或者更高,而应用程序的 targetSdkVersion
是 23 或者更高,则当应用程序请求危险权限时,系统会有如下行为:
如果应用程序当前在权限组中没有任何权限,则系统向描述应用程序希望访问的权限组的用户显示权限请求对话框。对话框没有描述该组中的特定权限。例如,如果一个应用程序请求READ_CONTACTS权限,系统对话框只告诉应用程序需要访问设备的联系人。如果用户给予批准,系统只会给应用程序它所请求的权限。
如果应用程序已经在同一权限组中被授予了另一个危险权限,系统会立即授予该权限,而不与用户进行任何交互。例如,如果一个应用程序之前请求并被授予了READ_CONTACTS权限,然后它请求WRITE_CONTACTS,系统立即授予该权限,而不向用户显示权限对话框。
上面是官方的原话,简而言之就是:属于同一组的危险权限将自动合并授予,用户授予应用某个权限组的权限,则应用将获得该权限组下的所有权限(前提是相关权限在 AndroidManifest.xml 中有声明)。
然而事实真的如此吗?我们来试验一下属于同一个权限组下的 READ_CONTACTS
权限和 WRITE_CONTACTS
权限。按照官方的说法,如果我先授权了 READ_CONTACTS
权限,那么 WRITE_CONTACTS
权限会被自动授予,我们来看看实际运行的效果:
可以看到,当我们先授予了 READ_CONTACTS
权限后,再去申请 WRITE_CONTACTS
权限时,依旧弹出了对话框让用户授权,这明显和官方文档说明的不一致。但是同时官方建议我们,不要将应用程序的逻辑建立在这些权限组的结构上,因为在未来的版本中,可能会将一个特定的权限从一个组移动到另一个组,因此,我们的代码逻辑不应该依赖权限组,而是应该显式地请求它需要的每个权限,即使用户已经在同一组中授予了另一个权限。
二、如何请求权限
每款 Android 应用都在访问受限的沙盒中运行。如果应用需要使用其自己的沙盒外的资源或信息,则必须请求相应权限。 要声明应用需要某项权限,可以在应用清单中列出该权限,然后在运行时请求用户批准每项权限(适用于 Android 6.0 及更高版本)。
(一)向清单文件添加权限
无论应用需要什么权限,都需要在清单文件中对权限进行声明。系统会根据声明权限的敏感程度采取不同的操作。有些权限被视为“常规”权限,系统会在安装应用时立即授予这些权限。还有些则被视为“危险”权限,需要用户明确授予相应访问权限。
(二)检查权限
如果应用需要一项危险权限,那么每次执行需要该权限的操作时,都必须检查自己是否具有该权限。从 Android 6.0(API 级别 23)开始,用户可随时从任何应用撤消权限,即使应用以较低的 API 级别为目标平台也是如此。因此,即使应用昨天使用了相机,也不能认为它今天仍具有该权限。
要检查应用是否具有某项权限,请调用 ContextCompat.checkSelfPermission()
方法。例如,以下代码段展示了如何检查 Activity 是否具有向日历写入数据的权限:
if (ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
}
如果应用具有此权限,该方法将返回 PERMISSION_GRANTED
,并且应用可以继续操作。如果应用不具备此权限,该方法将返回 PERMISSION_DENIED
,且应用必须明确要求用户授予权限。
(三)请求权限
当应用从 checkSelfPermission()
收到 PERMISSION_DENIED
时,需要提示用户授予该权限。Android 提供了几种可用来请求权限的方法(如 requestPermissions()),如下面的代码段所示。调用这些方法时,会显示一个无法自定义的标准 Android 对话框。
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Permission is not granted
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed; request the permission
ActivityCompat.requestPermissions(thisActivity,
new String[]{
Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
} else {
// Permission has already been granted
}
在某些情况下,需要帮助用户理解为什么应用需要某项权限。例如,如果用户启动一款摄影应用,用户或许不会对该应用请求使用相机的权限感到惊讶,但用户可能不理解为什么该应用想要访问用户的位置或联系人。在应用请求权限之前,可以向用户提供解释。一种比较好的做法是在用户之前拒绝过该权限请求的情况下提供解释。我们通过调用 shouldShowRequestPermissionRationale()
方法来实现。如果用户之前拒绝了该请求,该方法将返回 true。如果用户之前拒绝了该权限并且选中了权限请求对话框中的不再询问选项,或者如果设备政策禁止该权限,该方法将返回 false(注意,如果用户拒绝了该权限,并且勾选了“不再询问”,即使在返回false的逻辑中调用了requestPermissions方法,系统也不会再弹出选择框)。
(四)处理权限请求响应
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request.
}
}
三、自定义权限
(一)背景
Android 是一个特权分离的操作系统,其中每个应用程序都使用一个唯一的系统标识(Linux 用户 ID 和 组 ID)运行。系统也被称不同的部分,每个部分都有自己的标识。因此,Linux 将应用程序彼此隔离,并与系统隔离。应用程序可以自定义权限来提供给其他应用程序访问自己的功能。
(二)如何自定义权限
要自定义权限,可以在 AndroidManifest.xml
中使用 <permission>
标签来声明。
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp" >
<permission
android:name="com.example.myapp.permission.DEADLY_ACTIVITY"
android:label="@string/permlab_deadlyActivity"
android:description="@string/permdesc_deadlyActivity"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="dangerous" />
...
</manifest>
属性解释:
- name:自定义权限的名字。如果其他 app 引用该权限需要填写这个名字。
- lable:标签,用于描述该权限保护的关键功能(尽量简短)。显示给用户的,它的值可是一个 string 数据。
- description:描述,比 label 更长的对权限的描述。值是通过 resource 文件中获取的,不能直接写 string 值。
- permissionGroup:权限组,可选属性。在大多数情况下,应该将其设置为一个标准系统组(android.Manifest.permission_group),尽管可以自己定义一个组。
- protectionLevel:保护级别,它是必须的属性。
下面我们来写一个具体的例子:我们在进程1中定义一个 Activity,并为该 Activity 设置访问权限,然后让进程2来访问它。
进程1:
AndroidManifest.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chenyouyu.permissiondemo">
//自定义的权限,权限级别为 normal
<permission
android:name="com.example.myapp.permission.SECOND_ACTIVITY"
android:label="abc"
android:description="@string/permdesc_SecondActivity"
android:permissionGroup="android.permission-group.COST_MONEY"
android:protectionLevel="normal" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
//为SecondActivity加上android:permission="com.example.myapp.permission.SECOND_ACTIVITY"
<activity
android:name=".SecondActivity"
android:exported="true"
android:permission="com.example.myapp.permission.SECOND_ACTIVITY">
<intent-filter>
<action android:name="com.cyy.jump" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
......
</manifest>
进程2:
AndroidManifest.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chenyouyu.permissiondemo2">
//在AndroidManifest中声明权限
<uses-permission android:name="com.example.myapp.permission.SECOND_ACTIVITY"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
MainActivity.java
Intent intent = new Intent();
intent.setAction("com.cyy.jump");
intent.addCategory(Intent.CATEGORY_DEFAULT);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
首先运行进程1,然后运行进程2。
(三)自定义权限注意点
1.两个应用声明了相同的权限
Android 不允许两个不同的应用定义一个相同名字的权限(除非这两个应用拥有相同的签名),所以在命名的时候,需要特别注意。拥有相同自定义权限的软件必须使用同样的签名,否则后一个程序无法安装。
2.和应用安装顺序的关系。
场景:App A中声明了权限PermissionA,App B中使用了权限PermissionA。
情况一:PermissionA的保护级别是normal或者dangerous
App B先安装,App A后安装,此时App B无法获取PermissionA的权限,从App B打开App A会报权限错误。
App A先安装,App B后安装,从App B打开App A一切正常。
情况二:PermissionA的保护级别是signature或者signatureOrSystem
App B先安装,App A后安装,如果App A和App B是相同的签名,那么App B可以获取到PermissionA的权限。如果App A和App B的签名不同,则App B获取不到PermissionA权限。即,对于相同签名的app来说,不论安装先后,只要是声明了权限,请求该权限的app就会获得该权限。
这也说明了对于具有相同签名的系统app来说,安装过程不会考虑权限依赖的情况。安装系统app时,按照某个顺序(例如名字排序,目录位置排序等)安装即可,等所有app安装完了,所有使用权限的app都会获得权限。
3.权限的获取以及版本兼容
Android6.0引入了动态权限,这个大家都知道了。前面说到的自定义的权限的安全级别android:protectionLevel会影响权限在Android6.0+系统的使用
android:protectionLevel="normal",不需要动态申请
android:protectionLevel="dangerous",需要动态申请